Merge pull request #727 from sharelatex/ta-proxy-trailing-id

Proxy Dynamic Paths
This commit is contained in:
James Allen 2018-07-06 11:06:04 +01:00 committed by GitHub
commit f756ea5944
8 changed files with 125 additions and 38 deletions

View file

@ -4,33 +4,37 @@ request = require 'request'
URL = require 'url' URL = require 'url'
module.exports = ProxyManager = module.exports = ProxyManager =
call: (req, res, next) -> apply: (publicApiRouter) ->
requestUrl = URL.parse(req.url) for proxyUrl, target of settings.proxyUrls
requestPath = requestUrl.pathname # ignore the query part do (target) ->
publicApiRouter.get proxyUrl, ProxyManager.createProxy(target)
target = settings.proxyUrls[requestPath] createProxy: (target) ->
return next() unless target? # nothing to proxy (req, res, next) ->
targetUrl = makeTargetUrl(target, req)
logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url"
targetUrl = makeTargetUrl(target, req.query) upstream = request(targetUrl)
logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url" upstream.on "error", (error) ->
logger.error err: error, "error in ProxyManager"
upstream = request(targetUrl) # TODO: better handling of status code
upstream.on "error", (error) -> # see https://github.com/overleaf/write_latex/wiki/Streams-and-pipes-in-Node.js
logger.error err: error, "error in ProxyManager" upstream.pipe(res)
# TODO: better handling of status code
# see https://github.com/overleaf/write_latex/wiki/Streams-and-pipes-in-Node.js
upstream.pipe(res)
# make a URL from a proxy target. # make a URL from a proxy target.
# if the query is specified, set/replace the target's query with the given query # if the query is specified, set/replace the target's query with the given query
makeTargetUrl = (target, query) -> makeTargetUrl = (target, req) ->
targetUrl = URL.parse(parseSettingUrl(target)) targetUrl = URL.parse(parseSettingUrl(target, req))
if query? and Object.keys(query).length > 0 if req.query? and Object.keys(req.query).length > 0
targetUrl.query = query targetUrl.query = req.query
targetUrl.search = null # clear `search` as it takes precedence over `query` targetUrl.search = null # clear `search` as it takes precedence over `query`
targetUrl.format() targetUrl.format()
parseSettingUrl = (target) -> parseSettingUrl = (target, { params }) ->
return target if typeof target is 'string' return target if typeof target is 'string'
"#{target.baseUrl}#{target.path or ''}" if typeof target.path is 'function'
path = target.path(params)
else
path = target.path
"#{target.baseUrl}#{path or ''}"

View file

@ -74,7 +74,7 @@ app.use methodOverride()
app.use metrics.http.monitor(logger) app.use metrics.http.monitor(logger)
app.use RedirectManager app.use RedirectManager
app.use ProxyManager.call ProxyManager.apply(publicApiRouter)
webRouter.use cookieParser(Settings.security.sessionSecret) webRouter.use cookieParser(Settings.security.sessionSecret)

View file

@ -425,10 +425,6 @@ module.exports = settings =
redirects: redirects:
"/templates/index": "/templates/" "/templates/index": "/templates/"
proxyUrls:
'/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' }
'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
reloadModuleViewsOnEachRequest: true reloadModuleViewsOnEachRequest: true
domainLicences: [ domainLicences: [

View file

@ -0,0 +1,42 @@
should = require('chai').should()
assert = require('chai').assert
async = require("async")
request = require "./helpers/request"
MockV1Api = require "./helpers/MockV1Api"
assertResponse = (path, expectedStatusCode, expectedBody, cb) ->
request.get path, (error, response) ->
should.not.exist error
response.statusCode.should.equal expectedStatusCode
assert.deepEqual(JSON.parse(response.body), expectedBody) if expectedBody
cb()
describe "ProxyUrls", ->
before ->
@timeout(1000)
it 'proxy static URLs', (done) ->
async.series [
(cb) -> assertResponse '/institutions/list', 200, [], cb
(cb) -> assertResponse '/institutions/domains', 200, [], cb
],
done
it 'proxy dynamic URLs', (done) ->
async.series [
(cb) -> assertResponse '/institutions/list/123', 200, { id: 123 }, cb
(cb) -> assertResponse '/institutions/list/456', 200, { id: 456 }, cb
],
done
it 'return 404 if proxy is not set', (done) ->
async.series [
(cb) -> assertResponse '/institutions/foobar', 404, null, cb
],
done
it 'handle missing baseUrl', (done) ->
async.series [
(cb) -> assertResponse '/proxy/missing/baseUrl', 500, null, cb
],
done

View file

@ -51,6 +51,15 @@ module.exports = MockV1Api =
app.delete "/api/v2/users/:userId/affiliations/:email", (req, res, next) => app.delete "/api/v2/users/:userId/affiliations/:email", (req, res, next) =>
res.sendStatus 204 res.sendStatus 204
app.get '/universities/list', (req, res, next) ->
res.json []
app.get '/universities/list/:id', (req, res, next) ->
res.json id: parseInt(req.params.id)
app.get '/university/domains', (req, res, next) ->
res.json []
app.listen 5000, (error) -> app.listen 5000, (error) ->
throw error if error? throw error if error?
.on "error", (error) -> .on "error", (error) ->

View file

@ -1,3 +1,6 @@
v1Api =
url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
module.exports = module.exports =
enableSubscriptions: true enableSubscriptions: true
@ -93,3 +96,11 @@ module.exports =
collaborators: -1 collaborators: -1
dropbox: true dropbox: true
versioning: true versioning: true
proxyUrls:
'/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' }
'/institutions/list/:id':
baseUrl: v1Api.url
path: (params) -> "/universities/list/#{params.id}"
'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
'/proxy/missing/baseUrl': path: '/foo/bar'

View file

@ -9,6 +9,8 @@ class MockRequest
_parsedUrl:{} _parsedUrl:{}
i18n: i18n:
translate:-> translate:->
route:
path: ''
module.exports = MockRequest module.exports = MockRequest

View file

@ -24,9 +24,21 @@ describe "ProxyManager", ->
@res = new MockResponse() @res = new MockResponse()
@next = sinon.stub() @next = sinon.stub()
describe 'proxyUrls', -> describe 'apply', ->
it 'applies all paths', ->
@router = get: sinon.stub()
@settings.proxyUrls =
'/foo/bar': ''
'/foo/:id': ''
@proxyManager.apply(@router)
sinon.assert.calledTwice(@router.get)
assertCalledWith(@router.get, '/foo/bar')
assertCalledWith(@router.get, '/foo/:id')
describe 'createProxy', ->
beforeEach -> beforeEach ->
@req.url = @proxyPath @req.url = @proxyPath
@req.route.path = @proxyPath
@req.query = {} @req.query = {}
@settings.proxyUrls = {} @settings.proxyUrls = {}
@ -34,33 +46,44 @@ describe "ProxyManager", ->
@next.reset() @next.reset()
@request.reset() @request.reset()
it 'calls next when no match', ->
@proxyManager.call(@req, @res, @next)
sinon.assert.called(@next)
sinon.assert.notCalled(@request)
it 'does not calls next when match', -> it 'does not calls next when match', ->
@settings.proxyUrls[@proxyPath] = '/' target = '/'
@proxyManager.call(@req, @res, @next) @settings.proxyUrls[@proxyPath] = target
@proxyManager.createProxy(target)(@req, @res, @next)
sinon.assert.notCalled(@next) sinon.assert.notCalled(@next)
sinon.assert.called(@request) sinon.assert.called(@request)
it 'proxy full URL', -> it 'proxy full URL', ->
targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash' targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash'
@settings.proxyUrls[@proxyPath] = targetUrl @settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.call(@req) @proxyManager.createProxy(targetUrl)(@req)
assertCalledWith(@request, targetUrl) assertCalledWith(@request, targetUrl)
it 'overwrite query', -> it 'overwrite query', ->
targetUrl = 'foo.bar/baz?query' targetUrl = 'foo.bar/baz?query'
@req.query = { requestQuery: 'important' } @req.query = { requestQuery: 'important' }
@settings.proxyUrls[@proxyPath] = targetUrl @settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.call(@req) @proxyManager.createProxy(targetUrl)(@req)
newTargetUrl = 'foo.bar/baz?requestQuery=important' newTargetUrl = 'foo.bar/baz?requestQuery=important'
assertCalledWith(@request, newTargetUrl) assertCalledWith(@request, newTargetUrl)
it 'handles target objects', -> it 'handles target objects', ->
targetUrl = { baseUrl: 'api.v1', path: '/pa/th'} target = { baseUrl: 'api.v1', path: '/pa/th'}
@settings.proxyUrls[@proxyPath] = targetUrl @settings.proxyUrls[@proxyPath] = target
@proxyManager.call(@req, @res, @next) @proxyManager.createProxy(target)(@req, @res, @next)
assertCalledWith(@request, 'api.v1/pa/th') assertCalledWith(@request, 'api.v1/pa/th')
it 'handles missing baseUrl', ->
target = { path: '/pa/th'}
@settings.proxyUrls[@proxyPath] = target
@proxyManager.createProxy(target)(@req, @res, @next)
assertCalledWith(@request, 'undefined/pa/th')
it 'handles dynamic path', ->
target = baseUrl: 'api.v1', path: (params) -> "/resource/#{params.id}"
@settings.proxyUrls['/res/:id'] = target
@req.url = '/res/123'
@req.route.path = '/res/:id'
@req.params = id: 123
@proxyManager.createProxy(target)(@req, @res, @next)
assertCalledWith(@request, 'api.v1/resource/123')