Skip to content

Commit d77180a

Browse files
companion,companion-client: send uppy-versions header to companion
1 parent 0755798 commit d77180a

File tree

11 files changed

+371
-232
lines changed

11 files changed

+371
-232
lines changed

package-lock.json

+230-209
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@uppy/companion-client/src/RequestClient.js

+49-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ module.exports = class RequestClient {
1414
this.uppy = uppy
1515
this.opts = opts
1616
this.onReceiveResponse = this.onReceiveResponse.bind(this)
17+
this.allowedHeaders = []
18+
this.preflightDone = false
1719
}
1820

1921
get hostname () {
@@ -25,12 +27,15 @@ module.exports = class RequestClient {
2527
get defaultHeaders () {
2628
return {
2729
'Accept': 'application/json',
28-
'Content-Type': 'application/json'
30+
'Content-Type': 'application/json',
31+
'Uppy-Versions': '@uppy/companion-client=1.0.3'
2932
}
3033
}
3134

3235
headers () {
33-
return Promise.resolve(Object.assign({}, this.defaultHeaders, this.opts.serverHeaders || {}))
36+
return Promise.resolve(
37+
Object.assign({}, this.defaultHeaders, this.opts.serverHeaders || {})
38+
)
3439
}
3540

3641
_getPostResponseFunc (skip) {
@@ -77,9 +82,48 @@ module.exports = class RequestClient {
7782
return res.json()
7883
}
7984

85+
preflight (path) {
86+
return new Promise((resolve, reject) => {
87+
if (this.preflightDone) {
88+
return resolve(this.allowedHeaders.slice())
89+
}
90+
91+
fetch(this._getUrl(path), {
92+
method: 'OPTIONS'
93+
})
94+
.then((response) => {
95+
if (response.headers.has('access-control-allow-headers')) {
96+
const allowedHeaders = response.headers.get('access-control-allow-headers')
97+
.split(',').map((headerName) => headerName.trim().toLowerCase())
98+
this.allowedHeaders = this.allowedHeaders.concat(allowedHeaders)
99+
}
100+
this.preflightDone = true
101+
resolve(this.allowedHeaders.slice())
102+
})
103+
.catch(reject)
104+
})
105+
}
106+
107+
preflightAndHeaders (path) {
108+
return new Promise((resolve, reject) => {
109+
this.preflight(path).then((allowedHeaders) => {
110+
this.headers().then((headers) => {
111+
// filter to keep only allowed Headers
112+
Object.keys(headers).forEach((header) => {
113+
if (allowedHeaders.indexOf(header.toLowerCase()) === -1) {
114+
delete headers[header]
115+
}
116+
})
117+
118+
resolve(headers)
119+
})
120+
}).catch(reject)
121+
})
122+
}
123+
80124
get (path, skipPostResponse) {
81125
return new Promise((resolve, reject) => {
82-
this.headers().then((headers) => {
126+
this.preflightAndHeaders(path).then((headers) => {
83127
fetch(this._getUrl(path), {
84128
method: 'get',
85129
headers: headers,
@@ -97,7 +141,7 @@ module.exports = class RequestClient {
97141

98142
post (path, data, skipPostResponse) {
99143
return new Promise((resolve, reject) => {
100-
this.headers().then((headers) => {
144+
this.preflightAndHeaders(path).then((headers) => {
101145
fetch(this._getUrl(path), {
102146
method: 'post',
103147
headers: headers,
@@ -116,7 +160,7 @@ module.exports = class RequestClient {
116160

117161
delete (path, data, skipPostResponse) {
118162
return new Promise((resolve, reject) => {
119-
this.headers().then((headers) => {
163+
this.preflightAndHeaders(path).then((headers) => {
120164
fetch(`${this.hostname}/${path}`, {
121165
method: 'delete',
122166
headers: headers,

packages/@uppy/companion/package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@uppy/companion/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"purest": "3.0.0",
5757
"redis": "2.8.0",
5858
"request": "2.85.0",
59+
"semver": "6.1.1",
5960
"serialize-error": "^2.1.0",
6061
"tus-js-client": "^1.8.0-0",
6162
"uuid": "2.0.2",

packages/@uppy/companion/src/server/controllers/send-token.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const tokenService = require('../helpers/jwt')
66
const parseUrl = require('url').parse // eslint-disable-line node/no-deprecated-api
77
const { hasMatch, sanitizeHtml } = require('../helpers/utils')
88
const oAuthState = require('../helpers/oauth-state')
9+
const versionCmp = require('../helpers/version')
910

1011
/**
1112
*
@@ -29,8 +30,8 @@ module.exports = function sendToken (req, res, next) {
2930
const allowedClients = req.uppy.options.clients
3031
// if no preset clients then allow any client
3132
if (!allowedClients || hasMatch(origin, allowedClients) || hasMatch(parseUrl(origin).host, allowedClients)) {
32-
// @todo do a more secure client version check, see https://www.npmjs.com/package/semver
33-
return res.send(clientVersion ? htmlContent(uppyAuthToken, origin) : oldHtmlContent(uppyAuthToken, origin))
33+
const allowsStringMessage = versionCmp.gte(clientVersion, '1.0.2')
34+
return res.send(allowsStringMessage ? htmlContent(uppyAuthToken, origin) : oldHtmlContent(uppyAuthToken, origin))
3435
}
3536
}
3637
next()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const semver = require('semver')
2+
3+
/**
4+
* checks if a version is greater than or equal to
5+
* @param {string} v1 the LHS version
6+
* @param {string} v2 the RHS version
7+
* @returns {boolean}
8+
*/
9+
exports.gte = (v1, v2) => {
10+
v1 = semver.coerce(v1).version
11+
v2 = semver.coerce(v2).version
12+
13+
return semver.gte(v1, v2)
14+
}

packages/@uppy/companion/src/standalone/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const promInterval = collectDefaultMetrics({ register: promClient.register, time
2121

2222
// Add version as a prometheus gauge
2323
const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' })
24+
// @ts-ignore
2425
const numberVersion = version.replace(/\D/g, '') * 1
2526
versionGauge.set(numberVersion)
2627

packages/@uppy/companion/src/uppy.js

+25-10
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,34 @@ module.exports.app = (options = {}) => {
6363
app.use((req, res, next) => {
6464
res.header(
6565
'Access-Control-Allow-Headers',
66-
[res.get('Access-Control-Allow-Headers'), 'uppy-auth-token', 'uppy-client'].join(', ')
66+
[
67+
'uppy-auth-token',
68+
'uppy-versions',
69+
res.get('Access-Control-Allow-Headers')
70+
].join(',')
6771
)
68-
next()
69-
})
70-
if (options.sendSelfEndpoint) {
71-
app.use('*', (req, res, next) => {
72+
73+
const exposedHeaders = [
74+
// exposed so it can be accessed for our custom uppy preflight
75+
'Access-Control-Allow-Headers'
76+
]
77+
78+
if (options.sendSelfEndpoint) {
79+
// add it to the exposed headers.
80+
exposedHeaders.push('i-am')
81+
7282
const { protocol } = options.server
7383
res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`)
74-
// add it to the exposed custom headers.
75-
res.header('Access-Control-Expose-Headers', [res.get('Access-Control-Expose-Headers'), 'i-am'].join(', '))
76-
next()
77-
})
78-
}
84+
}
85+
86+
if (res.get('Access-Control-Expose-Headers')) {
87+
// if the header had been previously set, the values should be added too
88+
exposedHeaders.push(res.get('Access-Control-Expose-Headers'))
89+
}
90+
91+
res.header('Access-Control-Expose-Headers', exposedHeaders.join(','))
92+
next()
93+
})
7994

8095
// add uppy options to the request object so it can be accessed by subsequent handlers.
8196
app.use('*', getOptionsMiddleware(options))

packages/@uppy/companion/test/__tests__/companion.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,21 @@ jest.mock('../../src/server/helpers/oauth-state', () => {
66
return {
77
generateState: () => 'some-cool-nice-encrytpion',
88
addToState: () => 'some-cool-nice-encrytpion',
9-
getFromState: (state) => {
10-
return state === 'state-with-invalid-instance-url' ? 'http://localhost:3452' : 'http://localhost:3020'
9+
getFromState: (state, key) => {
10+
console.log('mummy', state, key)
11+
if (state === 'state-with-invalid-instance-url') {
12+
return 'http://localhost:3452'
13+
}
14+
15+
if (state === 'state-with-older-version' && key === 'clientVersion') {
16+
return '@uppy/companion-client:1.0.1'
17+
}
18+
19+
if (state === 'state-with-newer-version' && key === 'clientVersion') {
20+
return '@uppy/companion-client:1.0.3'
21+
}
22+
23+
return 'http://localhost:3020'
1124
}
1225
}
1326
})
@@ -76,13 +89,13 @@ describe('test authentication', () => {
7689
})
7790

7891
test('the token gets sent via cookie and html', () => {
92+
// see mock ../../src/server/helpers/oauth-state above for state values
7993
return request(authServer)
80-
.get(`/drive/send-token?uppyAuthToken=${token}`)
94+
.get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-newer-version`)
8195
.expect(200)
8296
.expect((res) => {
8397
const authToken = res.header['set-cookie'][0].split(';')[0].split('uppyAuthToken--google=')[1]
8498
expect(authToken).toEqual(token)
85-
// see mock ../../src/server/helpers/oauth-state above for http://localhost:3020
8699
const body = `
87100
<!DOCTYPE html>
88101
<html>
@@ -99,6 +112,28 @@ describe('test authentication', () => {
99112
})
100113
})
101114

115+
test('the token gets sent to html based on version', () => {
116+
// see mock ../../src/server/helpers/oauth-state above for state values
117+
return request(authServer)
118+
.get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-older-version`)
119+
.expect(200)
120+
.expect((res) => {
121+
const body = `
122+
<!DOCTYPE html>
123+
<html>
124+
<head>
125+
<meta charset="utf-8" />
126+
<script>
127+
window.opener.postMessage({token: "${token}"}, "http://localhost:3020")
128+
window.close()
129+
</script>
130+
</head>
131+
<body></body>
132+
</html>`
133+
expect(res.text).toBe(body)
134+
})
135+
})
136+
102137
test('logout provider', () => {
103138
return request(authServer)
104139
.get('/drive/logout/')

packages/@uppy/companion/test/mockserver.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ authServer.all('*/callback', (req, res, next) => {
1313
})
1414
authServer.all('/drive/send-token', (req, res, next) => {
1515
req.session.grant = {
16-
state: 'non-empty-value' }
16+
state: req.query.state || 'non-empty-value' }
1717
next()
1818
})
1919

packages/@uppy/companion/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"sourceMap": false,
88
"allowJs": true,
99
"checkJs": true,
10-
"noEmitOnError": true
10+
"noEmitOnError": true,
11+
"resolveJsonModule": true,
12+
"esModuleInterop": true
1113
},
1214
"include": [
1315
"src/**/*"

0 commit comments

Comments
 (0)