1 | const express = require('express');
|
2 | const qs = require('querystring');
|
3 | const companion = require('../companion');
|
4 | const helmet = require('helmet');
|
5 | const morgan = require('morgan');
|
6 | const bodyParser = require('body-parser');
|
7 | const redis = require('../server/redis');
|
8 | const logger = require('../server/logger');
|
9 | const { URL } = require('url');
|
10 | const merge = require('lodash.merge');
|
11 |
|
12 | const promBundle = require('express-prom-bundle');
|
13 | const session = require('express-session');
|
14 | const addRequestId = require('express-request-id')();
|
15 | const helper = require('./helper');
|
16 |
|
17 | const { version } = require('../../package.json');
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | function server(moreCompanionOptions = {}) {
|
24 | const app = express();
|
25 |
|
26 | let metricsMiddleware;
|
27 | if (process.env.COMPANION_HIDE_METRICS !== 'true') {
|
28 | metricsMiddleware = promBundle({ includeMethod: true });
|
29 |
|
30 | const promClient = metricsMiddleware.promClient;
|
31 | const collectDefaultMetrics = promClient.collectDefaultMetrics;
|
32 | collectDefaultMetrics({ register: promClient.register });
|
33 |
|
34 | const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' });
|
35 |
|
36 | const numberVersion = version.replace(/\D/g, '') * 1;
|
37 | versionGauge.set(numberVersion);
|
38 | }
|
39 |
|
40 | const sensitiveKeys = new Set(['access_token', 'uppyAuthToken']);
|
41 | |
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | function censorQuery(rawQuery) {
|
55 |
|
56 | const query = {};
|
57 | let censored = false;
|
58 | Object.keys(rawQuery).forEach((key) => {
|
59 | if (typeof rawQuery[key] !== 'string') {
|
60 | return;
|
61 | }
|
62 | if (sensitiveKeys.has(key)) {
|
63 |
|
64 | query[key] = '********';
|
65 | censored = true;
|
66 | }
|
67 | else {
|
68 | query[key] = rawQuery[key];
|
69 | }
|
70 | });
|
71 | return { query, censored };
|
72 | }
|
73 | app.use(addRequestId);
|
74 |
|
75 | app.use(morgan('combined'));
|
76 | morgan.token('url', (req, res) => {
|
77 | const { query, censored } = censorQuery(req.query);
|
78 | return censored ? `${req.path}?${qs.stringify(query)}` : req.originalUrl || req.url;
|
79 | });
|
80 | morgan.token('referrer', (req, res) => {
|
81 | const ref = req.headers.referer || req.headers.referrer;
|
82 | if (typeof ref === 'string') {
|
83 | const parsed = new URL(ref);
|
84 | const rawQuery = qs.parse(parsed.search.replace('?', ''));
|
85 | const { query, censored } = censorQuery(rawQuery);
|
86 | return censored ? `${parsed.href.split('?')[0]}?${qs.stringify(query)}` : parsed.href;
|
87 | }
|
88 | });
|
89 |
|
90 | if (process.env.COMPANION_HIDE_METRICS !== 'true') {
|
91 | app.use(metricsMiddleware);
|
92 | }
|
93 | app.use(bodyParser.json());
|
94 | app.use(bodyParser.urlencoded({ extended: false }));
|
95 |
|
96 | app.use(helmet.frameguard());
|
97 | app.use(helmet.xssFilter());
|
98 | app.use(helmet.noSniff());
|
99 | app.use(helmet.ieNoOpen());
|
100 | app.disable('x-powered-by');
|
101 | const companionOptions = helper.getCompanionOptions(moreCompanionOptions);
|
102 | const sessionOptions = {
|
103 | secret: companionOptions.secret,
|
104 | resave: true,
|
105 | saveUninitialized: true
|
106 | };
|
107 | if (companionOptions.redisUrl) {
|
108 | const RedisStore = require('connect-redis')(session);
|
109 | const redisClient = redis.client(merge({ url: companionOptions.redisUrl }, companionOptions.redisOptions));
|
110 | sessionOptions.store = new RedisStore({ client: redisClient });
|
111 | }
|
112 | if (process.env.COMPANION_COOKIE_DOMAIN) {
|
113 | sessionOptions.cookie = {
|
114 | domain: process.env.COMPANION_COOKIE_DOMAIN,
|
115 | maxAge: 24 * 60 * 60 * 1000
|
116 | };
|
117 | }
|
118 | app.use(session(sessionOptions));
|
119 | app.use((req, res, next) => {
|
120 | const protocol = process.env.COMPANION_PROTOCOL || 'http';
|
121 |
|
122 |
|
123 |
|
124 | if (process.env.COMPANION_CLIENT_ORIGINS) {
|
125 | const whitelist = process.env.COMPANION_CLIENT_ORIGINS
|
126 | .split(',')
|
127 | .map((url) => helper.hasProtocol(url) ? url : `${protocol}://${url}`);
|
128 |
|
129 | if (req.headers.origin && whitelist.indexOf(req.headers.origin) > -1) {
|
130 | res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
|
131 |
|
132 | res.setHeader('Access-Control-Allow-Credentials', 'true');
|
133 | }
|
134 | }
|
135 | else {
|
136 | res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
|
137 | }
|
138 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
139 | res.setHeader('Access-Control-Allow-Headers', 'Authorization, Origin, Content-Type, Accept');
|
140 | next();
|
141 | });
|
142 |
|
143 | if (process.env.COMPANION_HIDE_WELCOME !== 'true') {
|
144 | app.get('/', (req, res) => {
|
145 | res.setHeader('Content-Type', 'text/plain');
|
146 | res.send(helper.buildHelpfulStartupMessage(companionOptions));
|
147 | });
|
148 | }
|
149 | let companionApp;
|
150 | try {
|
151 |
|
152 | companionApp = companion.app(companionOptions);
|
153 | }
|
154 | catch (error) {
|
155 | console.error('\x1b[31m', error.message, '\x1b[0m');
|
156 | process.exit(1);
|
157 | }
|
158 |
|
159 | if (process.env.COMPANION_PATH) {
|
160 | app.use(process.env.COMPANION_PATH, companionApp);
|
161 | }
|
162 | else {
|
163 | app.use(companionApp);
|
164 | }
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | if (process.env.COMPANION_ONEDRIVE_DOMAIN_VALIDATION === 'true' && process.env.COMPANION_ONEDRIVE_KEY) {
|
171 | app.get('/.well-known/microsoft-identity-association.json', (req, res) => {
|
172 | const content = JSON.stringify({
|
173 | associatedApplications: [
|
174 | { applicationId: process.env.COMPANION_ONEDRIVE_KEY }
|
175 | ]
|
176 | });
|
177 | res.header('Content-Length', `${Buffer.byteLength(content, 'utf8')}`);
|
178 |
|
179 |
|
180 | res.writeHead(200, { 'Content-Type': 'application/json' });
|
181 | res.write(content);
|
182 | res.end();
|
183 | });
|
184 | }
|
185 | app.use((req, res, next) => {
|
186 | return res.status(404).json({ message: 'Not Found' });
|
187 | });
|
188 |
|
189 | app.use((err, req, res, next) => {
|
190 | const logStackTrace = true;
|
191 | if (app.get('env') === 'production') {
|
192 |
|
193 |
|
194 | if (err.status === 400 && err instanceof URIError) {
|
195 | logger.error(err.message, 'root.error', req.id);
|
196 | }
|
197 | else {
|
198 | logger.error(err, 'root.error', req.id, logStackTrace);
|
199 | }
|
200 | res.status(err.status || 500).json({ message: 'Something went wrong', requestId: req.id });
|
201 | }
|
202 | else {
|
203 | logger.error(err, 'root.error', req.id, logStackTrace);
|
204 | res.status(err.status || 500).json({ message: err.message, error: err, requestId: req.id });
|
205 | }
|
206 | });
|
207 | return { app, companionOptions };
|
208 | }
|
209 | const { app, companionOptions } = server();
|
210 | module.exports = { app, companionOptions, server };
|