diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index d9c7bfed03..3736b0b561 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -622,11 +622,6 @@ const ProjectController = { const projectId = req.params.Project_id - // record failures to load the custom websocket - if ((req.query != null ? req.query.ws : undefined) === 'fallback') { - metrics.inc('load-editor-ws-fallback') - } - async.auto( { project(cb) { @@ -777,6 +772,23 @@ const ProjectController = { allowedFreeTrial = !!subscription.freeTrial.allowed || true } + let wsUrl = Settings.wsUrl + let metricName = 'load-editor-ws' + if (user.betaProgram && Settings.wsUrlBeta !== undefined) { + wsUrl = Settings.wsUrlBeta + metricName += '-beta' + } + if (req.query && req.query.ws === 'fallback') { + // `?ws=fallback` will connect to the bare origin, and ignore + // the custom wsUrl. Hence it must load the client side + // javascript from there too. + // Not resetting it here would possibly load a socket.io v2 + // client and connect to a v0 endpoint. + wsUrl = undefined + metricName += '-fallback' + } + metrics.inc(metricName) + res.render('project/editor', { title: project.name, priority_title: true, @@ -831,6 +843,7 @@ const ProjectController = { brandVariation, allowedImageNames: Settings.allowedImageNames || [], gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl, + wsUrl, showSupport: Features.hasFeature('support') }) timer.done() diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index f29d053ee3..ff4cd2571a 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -66,7 +66,7 @@ html( script. window.sharelatex = { siteUrl: '#{settings.siteUrl}', - wsUrl: '#{settings.wsUrl}', + wsUrl: '#{wsUrl}', }; window.ab = {}; window.user_id = '#{getLoggedInUserId()}'; diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 56f89451ce..f243850339 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -160,7 +160,7 @@ block content window.overallThemes = JSON.parse('!{StringHelper.stringifyJsonForScript(overallThemes)}'); block foot-scripts - script(type="text/javascript" src='/socket.io/socket.io.js') + script(type="text/javascript" src=(wsUrl || '/socket.io') + '/socket.io.js') script(src=mathJaxPath) script(src=buildJsPath('libraries.js')) script(src=buildJsPath('ide.js')) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 5158fd43fc..fcdb517246 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -196,6 +196,7 @@ module.exports = settings = # Optional separate location for websocket connections, if unset defaults to siteUrl. wsUrl: process.env['WEBSOCKET_URL'] + wsUrlBeta: process.env['WEBSOCKET_URL_BETA'] # cookie domain # use full domain for cookies to only be accessible from that domain, diff --git a/services/web/frontend/js/ide/connection/ConnectionManager.js b/services/web/frontend/js/ide/connection/ConnectionManager.js index 5a7021bd72..bdbdaf481e 100644 --- a/services/web/frontend/js/ide/connection/ConnectionManager.js +++ b/services/web/frontend/js/ide/connection/ConnectionManager.js @@ -101,9 +101,11 @@ define([], function() { // initial connection attempt this.updateConnectionManagerState('connecting') + const parsedURL = new URL(this.wsUrl || '/socket.io', window.location) this.ide.socket = io.connect( - this.wsUrl, + parsedURL.origin, { + resource: parsedURL.pathname, reconnect: false, 'connect timeout': 30 * 1000, 'force new connection': true diff --git a/services/web/modules/launchpad/app/src/LaunchpadController.js b/services/web/modules/launchpad/app/src/LaunchpadController.js index 685a8b07e9..9ba914e344 100644 --- a/services/web/modules/launchpad/app/src/LaunchpadController.js +++ b/services/web/modules/launchpad/app/src/LaunchpadController.js @@ -66,6 +66,7 @@ module.exports = LaunchpadController = { } if (user && user.isAdmin) { return res.render(Path.resolve(__dirname, '../views/launchpad'), { + wsUrl: Settings.wsUrl, adminUserExists, authMethod }) diff --git a/services/web/modules/launchpad/app/views/launchpad.pug b/services/web/modules/launchpad/app/views/launchpad.pug index 146a90487a..eab04357c6 100644 --- a/services/web/modules/launchpad/app/views/launchpad.pug +++ b/services/web/modules/launchpad/app/views/launchpad.pug @@ -9,7 +9,7 @@ block content authMethod: "!{authMethod}" } - script(type="text/javascript" src='/socket.io/socket.io.js') + script(type="text/javascript" src=(wsUrl || '/socket.io') + '/socket.io.js') style. hr { margin-bottom: 5px; } diff --git a/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js b/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js index 4c47093e99..921a08d238 100644 --- a/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js +++ b/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js @@ -171,6 +171,7 @@ describe('LaunchpadController', function() { this.res.render.callCount.should.equal(1) return this.res.render .calledWith(viewPath, { + wsUrl: undefined, adminUserExists: true, authMethod: 'local' }) diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index 57ecc4f646..811ea906c2 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -129,6 +129,12 @@ describe('ProjectController', function() { } } ]) + this.Metrics = { + Timer: class { + done() {} + }, + inc: sinon.stub() + } this.ProjectController = SandboxedModule.require(MODULE_PATH, { globals: { @@ -140,12 +146,7 @@ describe('ProjectController', function() { log() {}, err() {} }, - 'metrics-sharelatex': { - Timer: class { - done() {} - }, - inc() {} - }, + 'metrics-sharelatex': this.Metrics, '@overleaf/o-error/http': HttpErrors, './ProjectDeleter': this.ProjectDeleter, './ProjectDuplicator': this.ProjectDuplicator, @@ -1145,6 +1146,81 @@ describe('ProjectController', function() { } this.ProjectController.loadEditor(this.req, this.res) }) + + describe('wsUrl', function() { + function checkLoadEditorWsMetric(metric) { + it(`should inc metric ${metric}`, function(done) { + this.res.render = () => { + this.Metrics.inc.calledWith(metric).should.equal(true) + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + } + function checkWsFallback(isBeta) { + describe('with ws=fallback', function() { + beforeEach(function() { + this.req.query = {} + this.req.query.ws = 'fallback' + }) + it('should unset the wsUrl', function(done) { + this.res.render = (pageName, opts) => { + ;(opts.wsUrl || '/socket.io').should.equal('/socket.io') + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + checkLoadEditorWsMetric( + `load-editor-ws${isBeta ? '-beta' : ''}-fallback` + ) + }) + } + + beforeEach(function() { + this.settings.wsUrl = '/other.socket.io' + }) + it('should set the custom wsUrl', function(done) { + this.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/other.socket.io') + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + checkLoadEditorWsMetric('load-editor-ws') + checkWsFallback(false) + + describe('beta program', function() { + beforeEach(function() { + this.settings.wsUrlBeta = '/beta.socket.io' + }) + describe('for a normal user', function() { + it('should set the normal custom wsUrl', function(done) { + this.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/other.socket.io') + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + checkLoadEditorWsMetric('load-editor-ws') + checkWsFallback(false) + }) + + describe('for a beta user', function() { + beforeEach(function() { + this.user.betaProgram = true + }) + it('should set the beta wsUrl', function(done) { + this.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/beta.socket.io') + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + checkLoadEditorWsMetric('load-editor-ws-beta') + checkWsFallback(true) + }) + }) + }) }) describe('userProjectsJson', function() {