6dcde1ec201b1a212beb704d99c61edebcd75be9
[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.manager.getRoot(req, res, ctx);
105 }
106
107
108 /**
109 * @param {http.ClientRequest} req
110 * @param {http.ServerResponse} res
111 * @param {Object} ctx
112 */
113 async handlerGetHealthcheck(req, res, ctx) {
114 const _scope = _fileScope('handlerGetHealthcheck');
115 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
116
117 Dingus.setHeadHandler(req, res, ctx);
118
119 this.setResponseType(this.responseTypes, req, res, ctx);
120
121 await this.manager.getHealthcheck(res, ctx);
122 }
123
124
125 /**
126 * @param {http.ClientRequest} req
127 * @param {http.ServerResponse} res
128 * @param {Object} ctx
129 */
130 async handlerGetInfo(req, res, ctx) {
131 const _scope = _fileScope('handlerGetInfo');
132 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
133
134 const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
135
136 Dingus.setHeadHandler(req, res, ctx);
137
138 this.setResponseType(responseTypes, req, res, ctx);
139
140 await this.manager.getInfo(res, ctx);
141 }
142
143
144 /**
145 * @param {http.ClientRequest} req
146 * @param {http.ServerResponse} res
147 * @param {Object} ctx
148 */
149 async handlerGetAdminOverview(req, res, ctx) {
150 const _scope = _fileScope('handlerGetAdminOverview');
151 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
152
153 Dingus.setHeadHandler(req, res, ctx);
154
155 this.setResponseType(this.responseTypes, req, res, ctx);
156
157 await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
158
159 await this.manager.getAdminOverview(res, ctx);
160 }
161
162
163 /**
164 * @param {http.ClientRequest} req
165 * @param {http.ServerResponse} res
166 * @param {Object} ctx
167 */
168 async handlerGetAdminTopicDetails(req, res, ctx) {
169 const _scope = _fileScope('handlerGetAdminTopicDetails');
170 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
171
172 Dingus.setHeadHandler(req, res, ctx);
173
174 this.setResponseType(this.responseTypes, req, res, ctx);
175
176 await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
177
178 await this.manager.getTopicDetails(res, ctx);
179 }
180
181
182 /**
183 * Similar to super.ingestBody, but if no body was sent, do not parse (and
184 * thus avoid possible unsupported media type error).
185 * Also removes raw body from context, to simplify scrubbing sensitive data from logs.
186 * @param {http.ClientRequest} req
187 * @param {http.ServerResponse} res
188 * @param {Object} ctx
189 */
190 async maybeIngestBody(req, res, ctx) {
191 ctx.rawBody = await this.bodyData(req);
192 const contentType = Dingus.getRequestContentType(req);
193 if (ctx.rawBody) {
194 this.parseBody(contentType, ctx);
195 delete ctx.rawBody;
196 }
197 }
198
199
200 /**
201 * @param {http.ClientRequest} req
202 * @param {http.ServerResponse} res
203 * @param {Object} ctx
204 */
205 async handlerUpdateTopic(req, res, ctx) {
206 const _scope = _fileScope('handlerUpdateTopic');
207 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
208
209 this.setResponseType(this.responseTypes, req, res, ctx);
210
211 await this.authenticator.apiRequiredLocal(req, res, ctx);
212
213 await this.maybeIngestBody(req, res, ctx);
214 ctx.method = req.method;
215 await this.manager.updateTopic(res, ctx);
216 }
217
218
219 /**
220 * @param {http.ClientRequest} req
221 * @param {http.ServerResponse} res
222 * @param {Object} ctx
223 */
224 async handlerUpdateSubscription(req, res, ctx) {
225 const _scope = _fileScope('handlerUpdateSubscription');
226 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
227
228 this.setResponseType(this.responseTypes, req, res, ctx);
229
230 await this.authenticator.apiRequiredLocal(req, res, ctx);
231
232 await this.maybeIngestBody(req, res, ctx);
233 ctx.method = req.method;
234 await this.manager.updateSubscription(res, ctx);
235 }
236
237
238 /**
239 * @param {http.ClientRequest} req
240 * @param {http.ServerResponse} res
241 * @param {Object} ctx
242 */
243 async handlerPostAdminProcess(req, res, ctx) {
244 const _scope = _fileScope('handlerPostAdminProcess');
245 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
246
247 this.setResponseType(this.responseTypes, req, res, ctx);
248
249 await this.authenticator.apiRequiredLocal(req, res, ctx);
250
251 await this.manager.processTasks(res, ctx);
252 }
253
254
255 /**
256 * Delegate login to authentication module.
257 * @param {http.ClientRequest} req
258 * @param {http.ServerResponse} res
259 * @param {Object} ctx
260 */
261 async handlerGetAdminLogin(req, res, ctx) {
262 const _scope = _fileScope('handlerGetAdminLogin');
263 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
264
265 Dingus.setHeadHandler(req, res, ctx);
266
267 this.setResponseType(this.responseTypes, req, res, ctx);
268
269 await this.sessionManager.getAdminLogin(res, ctx);
270 }
271
272
273 /**
274 * Delegate login to authentication module.
275 * @param {http.ClientRequest} req
276 * @param {http.ServerResponse} res
277 * @param {Object} ctx
278 */
279 async handlerPostAdminLogin(req, res, ctx) {
280 const _scope = _fileScope('handlerPostAdminLogin');
281 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
282
283 this.setResponseType(this.responseTypes, req, res, ctx);
284
285 await this.authenticator.sessionOptionalLocal(req, res, ctx);
286
287 await this.maybeIngestBody(req, res, ctx);
288
289 await this.sessionManager.postAdminLogin(res, ctx);
290 }
291
292
293 /**
294 * Delegate login to authentication module.
295 * @param {http.ClientRequest} req
296 * @param {http.ServerResponse} res
297 * @param {Object} ctx
298 */
299 async handlerGetAdminLogout(req, res, ctx) {
300 const _scope = _fileScope('handlerGetAdminLogout');
301 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
302
303 this.setResponseType(this.responseTypes, req, res, ctx);
304
305 await this.authenticator.sessionOptionalLocal(req, res, ctx);
306
307 await this.sessionManager.getAdminLogout(res, ctx);
308 }
309
310
311 /**
312 * Delegate login to authentication module.
313 * @param {http.ClientRequest} req
314 * @param {http.ServerResponse} res
315 * @param {Object} ctx
316 */
317 async handlerGetAdminIA(req, res, ctx) {
318 const _scope = _fileScope('handlerGetAdminIA');
319 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
320
321 this.setResponseType(this.responseTypes, req, res, ctx);
322
323 // Special case here, to see cookie before session established
324 ctx.cookie = req.getHeader(Enum.Header.Cookie);
325
326 await this.sessionManager.getAdminIA(res, ctx);
327 }
328
329 }
330
331 module.exports = Service;