Forráskód Böngészése

Merge pull request #1612 from transloadit/preflight-version-header

companion,companion-client: send uppy-versions header to companion
Ifedapo .A. Olarewaju 5 éve
szülő
commit
b25f243627

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 121 - 121
package-lock.json


+ 53 - 8
packages/@uppy/companion-client/src/RequestClient.js

@@ -14,6 +14,8 @@ module.exports = class RequestClient {
     this.uppy = uppy
     this.opts = opts
     this.onReceiveResponse = this.onReceiveResponse.bind(this)
+    this.allowedHeaders = ['accept', 'content-type', 'uppy-auth-token']
+    this.preflightDone = false
   }
 
   get hostname () {
@@ -25,12 +27,15 @@ module.exports = class RequestClient {
   get defaultHeaders () {
     return {
       'Accept': 'application/json',
-      'Content-Type': 'application/json'
+      'Content-Type': 'application/json',
+      'Uppy-Versions': '@uppy/companion-client=1.0.3'
     }
   }
 
   headers () {
-    return Promise.resolve(Object.assign({}, this.defaultHeaders, this.opts.serverHeaders || {}))
+    return Promise.resolve(
+      Object.assign({}, this.defaultHeaders, this.opts.serverHeaders || {})
+    )
   }
 
   _getPostResponseFunc (skip) {
@@ -77,9 +82,49 @@ module.exports = class RequestClient {
     return res.json()
   }
 
+  preflight (path) {
+    return new Promise((resolve, reject) => {
+      if (this.preflightDone) {
+        return resolve(this.allowedHeaders.slice())
+      }
+
+      fetch(this._getUrl(path), {
+        method: 'OPTIONS'
+      })
+        .then((response) => {
+          if (response.headers.has('access-control-allow-headers')) {
+            this.allowedHeaders = response.headers.get('access-control-allow-headers')
+              .split(',').map((headerName) => headerName.trim().toLowerCase())
+          }
+          this.preflightDone = true
+          resolve(this.allowedHeaders.slice())
+        })
+        .catch((err) => {
+          this.uppy.log(`[CompanionClient] unable to make preflight request ${err}`, 'warning')
+          this.preflightDone = true
+          resolve(this.allowedHeaders.slice())
+        })
+    })
+  }
+
+  preflightAndHeaders (path) {
+    return Promise.all([this.preflight(path), this.headers()])
+      .then(([allowedHeaders, headers]) => {
+        // filter to keep only allowed Headers
+        Object.keys(headers).forEach((header) => {
+          if (allowedHeaders.indexOf(header.toLowerCase()) === -1) {
+            this.uppy.log(`[CompanionClient] excluding unallowed header ${header}`)
+            delete headers[header]
+          }
+        })
+
+        return headers
+      })
+  }
+
   get (path, skipPostResponse) {
     return new Promise((resolve, reject) => {
-      this.headers().then((headers) => {
+      this.preflightAndHeaders(path).then((headers) => {
         fetch(this._getUrl(path), {
           method: 'get',
           headers: headers,
@@ -91,13 +136,13 @@ module.exports = class RequestClient {
             err = err.isAuthError ? err : new Error(`Could not get ${this._getUrl(path)}. ${err}`)
             reject(err)
           })
-      })
+      }).catch(reject)
     })
   }
 
   post (path, data, skipPostResponse) {
     return new Promise((resolve, reject) => {
-      this.headers().then((headers) => {
+      this.preflightAndHeaders(path).then((headers) => {
         fetch(this._getUrl(path), {
           method: 'post',
           headers: headers,
@@ -110,13 +155,13 @@ module.exports = class RequestClient {
             err = err.isAuthError ? err : new Error(`Could not post ${this._getUrl(path)}. ${err}`)
             reject(err)
           })
-      })
+      }).catch(reject)
     })
   }
 
   delete (path, data, skipPostResponse) {
     return new Promise((resolve, reject) => {
-      this.headers().then((headers) => {
+      this.preflightAndHeaders(path).then((headers) => {
         fetch(`${this.hostname}/${path}`, {
           method: 'delete',
           headers: headers,
@@ -129,7 +174,7 @@ module.exports = class RequestClient {
             err = err.isAuthError ? err : new Error(`Could not delete ${this._getUrl(path)}. ${err}`)
             reject(err)
           })
-      })
+      }).catch(reject)
     })
   }
 }

+ 5 - 0
packages/@uppy/companion/package-lock.json

@@ -52,6 +52,11 @@
 			"requires": {
 				"has-flag": "^3.0.0"
 			}
+		},
+		"semver": {
+			"version": "6.1.1",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz",
+			"integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ=="
 		}
 	}
 }

+ 1 - 0
packages/@uppy/companion/package.json

@@ -56,6 +56,7 @@
     "purest": "3.0.0",
     "redis": "2.8.0",
     "request": "2.85.0",
+    "semver": "6.1.1",
     "serialize-error": "^2.1.0",
     "tus-js-client": "^1.8.0-0",
     "uuid": "2.0.2",

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

@@ -6,6 +6,7 @@ const tokenService = require('../helpers/jwt')
 const parseUrl = require('url').parse // eslint-disable-line node/no-deprecated-api
 const { hasMatch, sanitizeHtml } = require('../helpers/utils')
 const oAuthState = require('../helpers/oauth-state')
+const versionCmp = require('../helpers/version')
 
 /**
  *
@@ -29,8 +30,8 @@ module.exports = function sendToken (req, res, next) {
     const allowedClients = req.uppy.options.clients
     // if no preset clients then allow any client
     if (!allowedClients || hasMatch(origin, allowedClients) || hasMatch(parseUrl(origin).host, allowedClients)) {
-      // @todo do a more secure client version check, see https://www.npmjs.com/package/semver
-      return res.send(clientVersion ? htmlContent(uppyAuthToken, origin) : oldHtmlContent(uppyAuthToken, origin))
+      const allowsStringMessage = versionCmp.gte(clientVersion, '1.0.2')
+      return res.send(allowsStringMessage ? htmlContent(uppyAuthToken, origin) : oldHtmlContent(uppyAuthToken, origin))
     }
   }
   next()

+ 14 - 0
packages/@uppy/companion/src/server/helpers/version.js

@@ -0,0 +1,14 @@
+const semver = require('semver')
+
+/**
+ * checks if a version is greater than or equal to
+ * @param {string} v1 the LHS version
+ * @param {string} v2 the RHS version
+ * @returns {boolean}
+ */
+exports.gte = (v1, v2) => {
+  v1 = semver.coerce(v1).version
+  v2 = semver.coerce(v2).version
+
+  return semver.gte(v1, v2)
+}

+ 1 - 0
packages/@uppy/companion/src/standalone/index.js

@@ -21,6 +21,7 @@ const promInterval = collectDefaultMetrics({ register: promClient.register, time
 
 // Add version as a prometheus gauge
 const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' })
+// @ts-ignore
 const numberVersion = version.replace(/\D/g, '') * 1
 versionGauge.set(numberVersion)
 

+ 27 - 11
packages/@uppy/companion/src/uppy.js

@@ -63,19 +63,34 @@ module.exports.app = (options = {}) => {
   app.use((req, res, next) => {
     res.header(
       'Access-Control-Allow-Headers',
-      [res.get('Access-Control-Allow-Headers'), 'uppy-auth-token', 'uppy-client'].join(', ')
+      [
+        'uppy-auth-token',
+        'uppy-versions',
+        res.get('Access-Control-Allow-Headers')
+      ].join(',')
     )
-    next()
-  })
-  if (options.sendSelfEndpoint) {
-    app.use('*', (req, res, next) => {
+
+    const exposedHeaders = [
+      // exposed so it can be accessed for our custom uppy preflight
+      'Access-Control-Allow-Headers'
+    ]
+
+    if (options.sendSelfEndpoint) {
+      // add it to the exposed headers.
+      exposedHeaders.push('i-am')
+
       const { protocol } = options.server
       res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`)
-      // add it to the exposed custom headers.
-      res.header('Access-Control-Expose-Headers', [res.get('Access-Control-Expose-Headers'), 'i-am'].join(', '))
-      next()
-    })
-  }
+    }
+
+    if (res.get('Access-Control-Expose-Headers')) {
+      // if the header had been previously set, the values should be added too
+      exposedHeaders.push(res.get('Access-Control-Expose-Headers'))
+    }
+
+    res.header('Access-Control-Expose-Headers', exposedHeaders.join(','))
+    next()
+  })
 
   // add uppy options to the request object so it can be accessed by subsequent handlers.
   app.use('*', getOptionsMiddleware(options))
@@ -214,11 +229,12 @@ const getOptionsMiddleware = (options) => {
    * @param {function} next
    */
   const middleware = (req, res, next) => {
+    const versionFromQuery = req.query.uppyVersions ? decodeURIComponent(req.query.uppyVersions) : null
     req.uppy = {
       options,
       s3Client,
       authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken,
-      clientVersion: req.header('uppy-versions') || req.query.uppyVersions,
+      clientVersion: req.header('uppy-versions') || versionFromQuery || '1.0.0',
       buildURL: getURLBuilder(options)
     }
     next()

+ 64 - 4
packages/@uppy/companion/test/__tests__/companion.js

@@ -6,8 +6,24 @@ jest.mock('../../src/server/helpers/oauth-state', () => {
   return {
     generateState: () => 'some-cool-nice-encrytpion',
     addToState: () => 'some-cool-nice-encrytpion',
-    getFromState: (state) => {
-      return state === 'state-with-invalid-instance-url' ? 'http://localhost:3452' : 'http://localhost:3020'
+    getFromState: (state, key) => {
+      if (state === 'state-with-invalid-instance-url') {
+        return 'http://localhost:3452'
+      }
+
+      if (state === 'state-with-older-version' && key === 'clientVersion') {
+        return '@uppy/companion-client=1.0.1'
+      }
+
+      if (state === 'state-with-newer-version' && key === 'clientVersion') {
+        return '@uppy/companion-client=1.0.3'
+      }
+
+      if (state === 'state-with-newer-version-old-style' && key === 'clientVersion') {
+        return 'companion-client:1.0.2'
+      }
+
+      return 'http://localhost:3020'
     }
   }
 })
@@ -76,13 +92,57 @@ describe('test authentication', () => {
   })
 
   test('the token gets sent via cookie and html', () => {
+    // see mock ../../src/server/helpers/oauth-state above for state values
     return request(authServer)
-      .get(`/drive/send-token?uppyAuthToken=${token}`)
+      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-newer-version`)
       .expect(200)
       .expect((res) => {
         const authToken = res.header['set-cookie'][0].split(';')[0].split('uppyAuthToken--google=')[1]
         expect(authToken).toEqual(token)
-        // see mock ../../src/server/helpers/oauth-state above for http://localhost:3020
+        const body = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+        <meta charset="utf-8" />
+        <script>
+          window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
+          window.close()
+        </script>
+    </head>
+    <body></body>
+    </html>`
+        expect(res.text).toBe(body)
+      })
+  })
+
+  test('the token gets to older clients without stringify', () => {
+    // see mock ../../src/server/helpers/oauth-state above for state values
+    return request(authServer)
+      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-older-version`)
+      .expect(200)
+      .expect((res) => {
+        const body = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+        <meta charset="utf-8" />
+        <script>
+          window.opener.postMessage({token: "${token}"}, "http://localhost:3020")
+          window.close()
+        </script>
+    </head>
+    <body></body>
+    </html>`
+        expect(res.text).toBe(body)
+      })
+  })
+
+  test('the token gets sent to newer clients with old version style', () => {
+    // see mock ../../src/server/helpers/oauth-state above for state values
+    return request(authServer)
+      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-newer-version-old-style`)
+      .expect(200)
+      .expect((res) => {
         const body = `
     <!DOCTYPE html>
     <html>

+ 1 - 1
packages/@uppy/companion/test/mockserver.js

@@ -13,7 +13,7 @@ authServer.all('*/callback', (req, res, next) => {
 })
 authServer.all('/drive/send-token', (req, res, next) => {
   req.session.grant = {
-    state: 'non-empty-value' }
+    state: req.query.state || 'non-empty-value' }
   next()
 })
 

+ 1 - 1
packages/@uppy/provider-views/src/index.js

@@ -423,7 +423,7 @@ module.exports = class ProviderView {
   handleAuth () {
     const authState = btoa(JSON.stringify({ origin: getOrigin() }))
     // @todo remove this hardcoded version
-    const clientVersion = 'companion-client:1.0.2'
+    const clientVersion = encodeURIComponent('@uppy/companion-client=1.0.2')
     const link = `${this.provider.authUrl()}?state=${authState}&uppyVersions=${clientVersion}`
 
     const authWindow = window.open(link, '_blank')

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott