-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathindex.js
217 lines (192 loc) · 7.51 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
const express = require('express')
const qs = require('querystring')
const helmet = require('helmet')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const { URL } = require('url')
const merge = require('lodash.merge')
const session = require('express-session')
const addRequestId = require('express-request-id')()
const logger = require('../server/logger')
const redis = require('../server/redis')
const companion = require('../companion')
const helper = require('./helper')
const middlewares = require('../server/middlewares')
const { getURLBuilder } = require('../server/helpers/utils')
/**
* Configures an Express app for running Companion standalone
*
* @returns {object}
*/
module.exports = function server (inputCompanionOptions = {}) {
let corsOrigins
if (process.env.COMPANION_CLIENT_ORIGINS) {
corsOrigins = process.env.COMPANION_CLIENT_ORIGINS
.split(',')
.map((url) => (helper.hasProtocol(url) ? url : `${process.env.COMPANION_PROTOCOL || 'http'}://${url}`))
} else if (process.env.COMPANION_CLIENT_ORIGINS_REGEX) {
corsOrigins = new RegExp(process.env.COMPANION_CLIENT_ORIGINS_REGEX)
}
const moreCompanionOptions = { ...inputCompanionOptions, corsOrigins }
const companionOptions = helper.getCompanionOptions(moreCompanionOptions)
const app = express()
const router = express.Router()
if (companionOptions.server.path) {
app.use(companionOptions.server.path, router)
} else {
app.use(router)
}
// Query string keys whose values should not end up in logging output.
const sensitiveKeys = new Set(['access_token', 'uppyAuthToken'])
/**
* Obscure the contents of query string keys listed in `sensitiveKeys`.
*
* Returns a copy of the object with unknown types removed and sensitive values replaced by ***.
*
* The input type is more broad that it needs to be, this way typescript can help us guarantee that we're dealing with all
* possible inputs :)
*
* @param {Record<string, any>} rawQuery
* @returns {{
* query: Record<string, any>,
* censored: boolean
* }}
*/
function censorQuery (rawQuery) {
/** @type {Record<string, any>} */
const query = {}
let censored = false
Object.keys(rawQuery).forEach((key) => {
if (typeof rawQuery[key] !== 'string') {
return
}
if (sensitiveKeys.has(key)) {
// replace logged access token
query[key] = '********'
censored = true
} else {
query[key] = rawQuery[key]
}
})
return { query, censored }
}
router.use(addRequestId)
// log server requests.
router.use(morgan('combined'))
morgan.token('url', (req) => {
const { query, censored } = censorQuery(req.query)
return censored ? `${req.path}?${qs.stringify(query)}` : req.originalUrl || req.url
})
morgan.token('referrer', (req) => {
const ref = req.headers.referer || req.headers.referrer
if (typeof ref === 'string') {
let parsed
try {
parsed = new URL(ref)
} catch (_) {
return ref
}
const rawQuery = qs.parse(parsed.search.replace('?', ''))
const { query, censored } = censorQuery(rawQuery)
return censored ? `${parsed.href.split('?')[0]}?${qs.stringify(query)}` : parsed.href
}
})
// for server metrics tracking.
// make app metrics available at '/metrics'.
// TODO for the next major version: use instead companion option "metrics": true and remove this code
// eslint-disable-next-line max-len
// See discussion: https://github.com/transloadit/uppy/pull/2854/files/64be97205e4012818abfcc8b0b8b7fe09de91729#diff-68f5e3eb307c1c9d1fd02224fd7888e2f74718744e1b6e35d929fcab1cc50ed1
if (process.env.COMPANION_HIDE_METRICS !== 'true') {
router.use(middlewares.metrics({ path: companionOptions.server.path }))
// backward compatibility
// TODO remove in next major semver
if (companionOptions.server.path) {
const buildUrl = getURLBuilder(companionOptions)
app.get('/metrics', (req, res) => {
process.emitWarning('/metrics is deprecated when specifying a path to companion')
const metricsUrl = buildUrl('/metrics', true)
res.redirect(metricsUrl)
})
}
}
router.use(bodyParser.json())
router.use(bodyParser.urlencoded({ extended: false }))
// Use helmet to secure Express headers
router.use(helmet.frameguard())
router.use(helmet.xssFilter())
router.use(helmet.noSniff())
router.use(helmet.ieNoOpen())
app.disable('x-powered-by')
const sessionOptions = {
secret: companionOptions.secret,
resave: true,
saveUninitialized: true,
}
if (companionOptions.redisUrl) {
const RedisStore = require('connect-redis')(session)
const redisClient = redis.client(
merge({ url: companionOptions.redisUrl }, companionOptions.redisOptions),
)
sessionOptions.store = new RedisStore({ client: redisClient })
}
if (process.env.COMPANION_COOKIE_DOMAIN) {
sessionOptions.cookie = {
domain: process.env.COMPANION_COOKIE_DOMAIN,
maxAge: 24 * 60 * 60 * 1000, // 1 day
}
}
router.use(session(sessionOptions))
// Routes
if (process.env.COMPANION_HIDE_WELCOME !== 'true') {
router.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send(helper.buildHelpfulStartupMessage(companionOptions))
})
}
// initialize companion
const companionApp = companion.app(companionOptions)
// add companion to server middleware
router.use(companionApp)
// WARNING: This route is added in order to validate your app with OneDrive.
// Only set COMPANION_ONEDRIVE_DOMAIN_VALIDATION if you are sure that you are setting the
// correct value for COMPANION_ONEDRIVE_KEY (i.e application ID). If there's a slightest possiblilty
// that you might have mixed the values for COMPANION_ONEDRIVE_KEY and COMPANION_ONEDRIVE_SECRET,
// please DO NOT set any value for COMPANION_ONEDRIVE_DOMAIN_VALIDATION
if (process.env.COMPANION_ONEDRIVE_DOMAIN_VALIDATION === 'true' && process.env.COMPANION_ONEDRIVE_KEY) {
router.get('/.well-known/microsoft-identity-association.json', (req, res) => {
const content = JSON.stringify({
associatedApplications: [
{ applicationId: process.env.COMPANION_ONEDRIVE_KEY },
],
})
res.header('Content-Length', `${Buffer.byteLength(content, 'utf8')}`)
// use writeHead to prevent 'charset' from being appended
// eslint-disable-next-line max-len
// https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-configure-publisher-domain#to-select-a-verified-domain
res.writeHead(200, { 'Content-Type': 'application/json' })
res.write(content)
res.end()
})
}
app.use((req, res) => {
return res.status(404).json({ message: 'Not Found' })
})
// @ts-ignore
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
const logStackTrace = true
if (app.get('env') === 'production') {
// if the error is a URIError from the requested URL we only log the error message
// to avoid uneccessary error alerts
if (err.status === 400 && err instanceof URIError) {
logger.error(err.message, 'root.error', req.id)
} else {
logger.error(err, 'root.error', req.id, logStackTrace)
}
res.status(err.status || 500).json({ message: 'Something went wrong', requestId: req.id })
} else {
logger.error(err, 'root.error', req.id, logStackTrace)
res.status(err.status || 500).json({ message: err.message, error: err, requestId: req.id })
}
})
return { app, companionOptions }
}