root html is no longer invariant, serve it normally
[websub-hub] / src / service.js
1 'use strict';
2
3 /**
4 * Here we extend the base API server to define our routes and any route-specific
5 * behavior (middlewares) before handing off to the manager.
6 */
7
8 const { Dingus } = require('@squeep/api-dingus');
9 const common = require('./common');
10 const Enum = require('./enum');
11 const Manager = require('./manager');
12 const { Authenticator, SessionManager } = require('@squeep/authentication-module');
13 const path = require('path');
14
15 const _fileScope = common.fileScope(__filename);
16
17 class Service extends Dingus {
18 constructor(logger, db, options) {
19 super(logger, {
20 ...options.dingus,
21 ignoreTrailingSlash: false,
22 });
23
24 this.manager = new Manager(logger, db, options);
25 this.authenticator = new Authenticator(logger, db, options);
26 this.sessionManager = new SessionManager(logger, this.authenticator, options);
27 this.staticPath = path.join(__dirname, '..', 'static');
28 this.loginPath = `${options.dingus.proxyPrefix}/admin/login`;
29
30 // Primary API endpoint
31 this.on('POST', '/', this.handlerPostRoot.bind(this));
32
33 // Information page about service
34 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot.bind(this));
35
36 // Give load-balancers something to check
37 this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck.bind(this));
38
39 // Public information about topics
40 this.on('GET', '/info', this.handlerGetInfo.bind(this));
41 this.on('GET', '/info/', this.handlerGetInfo.bind(this));
42
43 // These routes are intended for accessing static content during development.
44 // In production, a proxy server would likely handle these first.
45 this.on(['GET', 'HEAD'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
46 this.on(['GET', 'HEAD'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
47 this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile.bind(this));
48 this.on(['GET', 'HEAD'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
49 this.on(['GET', 'HEAD'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
50
51 // Private informational endpoints
52 this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
53 this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this));
54 this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
55
56 // Private data-editing endpoints
57 this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this));
58 this.on(['PATCH', 'DELETE'], '/admin/subscription/:subscriptionId', this.handlerUpdateSubscription.bind(this));
59
60 // Private server-action endpoints
61 this.on('POST', '/admin/process', this.handlerPostAdminProcess.bind(this));
62
63 // Admin login
64 this.on(['GET', 'HEAD'], '/admin/login', this.handlerGetAdminLogin.bind(this));
65 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin.bind(this));
66 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout.bind(this));
67 this.on(['GET'], '/admin/_ia', this.handlerGetAdminIA.bind(this));
68
69 }
70
71
72 /**
73 * @param {http.ClientRequest} req
74 * @param {http.ServerResponse} res
75 * @param {Object} ctx
76 */
77 async handlerPostRoot(req, res, ctx) {
78 const _scope = _fileScope('handlerPostRoot');
79 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
80
81 this.setResponseType(this.responseTypes, req, res, ctx);
82 await this.ingestBody(req, res, ctx);
83
84 await this.manager.postRoot(req, res, ctx);
85 }
86
87
88 /**
89 * @param {http.ClientRequest} req
90 * @param {http.ServerResponse} res
91 * @param {Object} ctx
92 */
93 async handlerGetRoot(req, res, ctx) {
94 const _scope = _fileScope('handlerGetRoot');
95 const responseTypes = [
96 Enum.ContentType.TextHTML,
97 ];
98 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
99
100 Dingus.setHeadHandler(req, res, ctx);
101
102 this.setResponseType(responseTypes, req, res, ctx);
103
104 await this.authenticator.sessionOptional(req, res, ctx, this.loginPath);
105
106 await this.manager.getRoot(req, res, ctx);
107 }
108
109
110 /**
111 * @param {http.ClientRequest} req
112 * @param {http.ServerResponse} res
113 * @param {Object} ctx
114 */
115 async handlerGetHealthcheck(req, res, ctx) {
116 const _scope = _fileScope('handlerGetHealthcheck');
117 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
118
119 Dingus.setHeadHandler(req, res, ctx);
120
121 this.setResponseType(this.responseTypes, req, res, ctx);
122
123 await this.manager.getHealthcheck(res, ctx);
124 }
125
126
127 /**
128 * @param {http.ClientRequest} req
129 * @param {http.ServerResponse} res
130 * @param {Object} ctx
131 */
132 async handlerGetInfo(req, res, ctx) {
133 const _scope = _fileScope('handlerGetInfo');
134 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
135
136 const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
137
138 Dingus.setHeadHandler(req, res, ctx);
139
140 this.setResponseType(responseTypes, req, res, ctx);
141
142 await this.manager.getInfo(res, ctx);
143 }
144
145
146 /**
147 * @param {http.ClientRequest} req
148 * @param {http.ServerResponse} res
149 * @param {Object} ctx
150 */
151 async handlerGetAdminOverview(req, res, ctx) {
152 const _scope = _fileScope('handlerGetAdminOverview');
153 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
154
155 Dingus.setHeadHandler(req, res, ctx);
156
157 this.setResponseType(this.responseTypes, req, res, ctx);
158
159 await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
160
161 await this.manager.getAdminOverview(res, ctx);
162 }
163
164
165 /**
166 * @param {http.ClientRequest} req
167 * @param {http.ServerResponse} res
168 * @param {Object} ctx
169 */
170 async handlerGetAdminTopicDetails(req, res, ctx) {
171 const _scope = _fileScope('handlerGetAdminTopicDetails');
172 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
173
174 Dingus.setHeadHandler(req, res, ctx);
175
176 this.setResponseType(this.responseTypes, req, res, ctx);
177
178 await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
179
180 await this.manager.getTopicDetails(res, ctx);
181 }
182
183
184 /**
185 * Similar to super.ingestBody, but if no body was sent, do not parse (and
186 * thus avoid possible unsupported media type error).
187 * Also removes raw body from context, to simplify scrubbing sensitive data from logs.
188 * @param {http.ClientRequest} req
189 * @param {http.ServerResponse} res
190 * @param {Object} ctx
191 */
192 async maybeIngestBody(req, res, ctx) {
193 ctx.rawBody = await this.bodyData(req);
194 const contentType = Dingus.getRequestContentType(req);
195 if (ctx.rawBody) {
196 this.parseBody(contentType, ctx);
197 delete ctx.rawBody;
198 }
199 }
200
201
202 /**
203 * @param {http.ClientRequest} req
204 * @param {http.ServerResponse} res
205 * @param {Object} ctx
206 */
207 async handlerUpdateTopic(req, res, ctx) {
208 const _scope = _fileScope('handlerUpdateTopic');
209 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
210
211 this.setResponseType(this.responseTypes, req, res, ctx);
212
213 await this.authenticator.apiRequiredLocal(req, res, ctx);
214
215 await this.maybeIngestBody(req, res, ctx);
216 ctx.method = req.method;
217 await this.manager.updateTopic(res, ctx);
218 }
219
220
221 /**
222 * @param {http.ClientRequest} req
223 * @param {http.ServerResponse} res
224 * @param {Object} ctx
225 */
226 async handlerUpdateSubscription(req, res, ctx) {
227 const _scope = _fileScope('handlerUpdateSubscription');
228 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
229
230 this.setResponseType(this.responseTypes, req, res, ctx);
231
232 await this.authenticator.apiRequiredLocal(req, res, ctx);
233
234 await this.maybeIngestBody(req, res, ctx);
235 ctx.method = req.method;
236 await this.manager.updateSubscription(res, ctx);
237 }
238
239
240 /**
241 * @param {http.ClientRequest} req
242 * @param {http.ServerResponse} res
243 * @param {Object} ctx
244 */
245 async handlerPostAdminProcess(req, res, ctx) {
246 const _scope = _fileScope('handlerPostAdminProcess');
247 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
248
249 this.setResponseType(this.responseTypes, req, res, ctx);
250
251 await this.authenticator.apiRequiredLocal(req, res, ctx);
252
253 await this.manager.processTasks(res, ctx);
254 }
255
256
257 /**
258 * Delegate login to authentication module.
259 * @param {http.ClientRequest} req
260 * @param {http.ServerResponse} res
261 * @param {Object} ctx
262 */
263 async handlerGetAdminLogin(req, res, ctx) {
264 const _scope = _fileScope('handlerGetAdminLogin');
265 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
266
267 Dingus.setHeadHandler(req, res, ctx);
268
269 this.setResponseType(this.responseTypes, req, res, ctx);
270
271 await this.sessionManager.getAdminLogin(res, ctx);
272 }
273
274
275 /**
276 * Delegate login to authentication module.
277 * @param {http.ClientRequest} req
278 * @param {http.ServerResponse} res
279 * @param {Object} ctx
280 */
281 async handlerPostAdminLogin(req, res, ctx) {
282 const _scope = _fileScope('handlerPostAdminLogin');
283 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
284
285 this.setResponseType(this.responseTypes, req, res, ctx);
286
287 await this.authenticator.sessionOptionalLocal(req, res, ctx);
288
289 await this.maybeIngestBody(req, res, ctx);
290
291 await this.sessionManager.postAdminLogin(res, ctx);
292 }
293
294
295 /**
296 * Delegate login to authentication module.
297 * @param {http.ClientRequest} req
298 * @param {http.ServerResponse} res
299 * @param {Object} ctx
300 */
301 async handlerGetAdminLogout(req, res, ctx) {
302 const _scope = _fileScope('handlerGetAdminLogout');
303 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
304
305 this.setResponseType(this.responseTypes, req, res, ctx);
306
307 await this.authenticator.sessionOptionalLocal(req, res, ctx);
308
309 await this.sessionManager.getAdminLogout(res, ctx);
310 }
311
312
313 /**
314 * Delegate login to authentication module.
315 * @param {http.ClientRequest} req
316 * @param {http.ServerResponse} res
317 * @param {Object} ctx
318 */
319 async handlerGetAdminIA(req, res, ctx) {
320 const _scope = _fileScope('handlerGetAdminIA');
321 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
322
323 this.setResponseType(this.responseTypes, req, res, ctx);
324
325 // Special case here, to see cookie before session established
326 ctx.cookie = req.getHeader(Enum.Header.Cookie);
327
328 await this.sessionManager.getAdminIA(res, ctx);
329 }
330
331 }
332
333 module.exports = Service;