mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #727 from sharelatex/ta-proxy-trailing-id
Proxy Dynamic Paths
This commit is contained in:
commit
f756ea5944
8 changed files with 125 additions and 38 deletions
|
@ -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 ''}"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
42
services/web/test/acceptance/coffee/ProxyUrls.coffee
Normal file
42
services/web/test/acceptance/coffee/ProxyUrls.coffee
Normal 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
|
|
@ -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) ->
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -9,6 +9,8 @@ class MockRequest
|
||||||
_parsedUrl:{}
|
_parsedUrl:{}
|
||||||
i18n:
|
i18n:
|
||||||
translate:->
|
translate:->
|
||||||
|
route:
|
||||||
|
path: ''
|
||||||
|
|
||||||
module.exports = MockRequest
|
module.exports = MockRequest
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue