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