Skip to content

Commit

Permalink
feat: upstreamProxy config option to deal with proxies that adjust th…
Browse files Browse the repository at this point in the history
…e base path, etc
  • Loading branch information
pghalliday committed Sep 7, 2016
1 parent 98a4fbf commit 55755e4
Show file tree
Hide file tree
Showing 16 changed files with 353 additions and 96 deletions.
1 change: 1 addition & 0 deletions client/constants.js
@@ -1,5 +1,6 @@
module.exports = {
VERSION: '%KARMA_VERSION%',
KARMA_URL_ROOT: '%KARMA_URL_ROOT%',
KARMA_PROXY_PATH: '%KARMA_PROXY_PATH%',
CONTEXT_URL: 'context.html'
}
6 changes: 4 additions & 2 deletions client/main.js
Expand Up @@ -5,15 +5,17 @@ require('core-js/es5')
var Karma = require('./karma')
var StatusUpdater = require('./updater')
var util = require('../common/util')
var constants = require('./constants')

var KARMA_URL_ROOT = require('./constants').KARMA_URL_ROOT
var KARMA_URL_ROOT = constants.KARMA_URL_ROOT
var KARMA_PROXY_PATH = constants.KARMA_PROXY_PATH

// Connect to the server using socket.io http://socket.io
var socket = io(location.host, {
reconnectionDelay: 500,
reconnectionDelayMax: Infinity,
timeout: 2000,
path: '/' + KARMA_URL_ROOT.substr(1) + 'socket.io',
path: KARMA_PROXY_PATH + KARMA_URL_ROOT.substr(1) + 'socket.io',
'sync disconnect on unload': true
})

Expand Down
38 changes: 38 additions & 0 deletions docs/config/01-configuration-file.md
Expand Up @@ -647,6 +647,44 @@ is handed off to [socket.io](http://socket.io/) (which manages the communication
between browsers and the testing server).


## upstreamProxy
**Type:** Object

**Default:** `undefined`

**Description:** For use when the Karma server needs to be run behind a proxy that changes the base url, etc

If set then the following fields will be defined and can be overriden:

### path
**Type:** String

**Default:** `'/'`

**Description:** Will be prepended to the base url when launching browsers and prepended to internal urls as loaded by the browsers

### port
**Type:** Number

**Default:** `9875`

**Description:** Will be used as the port when launching browsers

### hostname
**Type:** String

**Default:** `'localhost'`

**Description:** Will be used as the hostname when launching browsers

### protocol
**Type:** String

**Default:** `'http:'`

**Description:** Will be used as the protocol when launching browsers


## urlRoot
**Type:** String

Expand Down
45 changes: 38 additions & 7 deletions lib/config.js
Expand Up @@ -72,24 +72,38 @@ var createPatternObject = function (pattern) {
return new Pattern(null, false, false, false, false)
}

var normalizeUrlRoot = function (urlRoot) {
var normalizedUrlRoot = urlRoot

if (normalizedUrlRoot.charAt(0) !== '/') {
normalizedUrlRoot = '/' + normalizedUrlRoot
var normalizeUrl = function (url) {
if (url.charAt(0) !== '/') {
url = '/' + url
}

if (normalizedUrlRoot.charAt(normalizedUrlRoot.length - 1) !== '/') {
normalizedUrlRoot = normalizedUrlRoot + '/'
if (url.charAt(url.length - 1) !== '/') {
url = url + '/'
}

return url
}

var normalizeUrlRoot = function (urlRoot) {
var normalizedUrlRoot = normalizeUrl(urlRoot)

if (normalizedUrlRoot !== urlRoot) {
log.warn('urlRoot normalized to "%s"', normalizedUrlRoot)
}

return normalizedUrlRoot
}

var normalizeProxyPath = function (proxyPath) {
var normalizedProxyPath = normalizeUrl(proxyPath)

if (normalizedProxyPath !== proxyPath) {
log.warn('proxyPath normalized to "%s"', normalizedProxyPath)
}

return normalizedProxyPath
}

var normalizeConfig = function (config, configFilePath) {
var basePathResolve = function (relativePath) {
if (helper.isUrlAbsolute(relativePath)) {
Expand Down Expand Up @@ -135,6 +149,22 @@ var normalizeConfig = function (config, configFilePath) {
// normalize urlRoot
config.urlRoot = normalizeUrlRoot(config.urlRoot)

// normalize and default upstream proxy settings if given
if (config.upstreamProxy) {
var proxy = config.upstreamProxy
proxy.path = _.isUndefined(proxy.path) ? '/' : normalizeProxyPath(proxy.path)
proxy.hostname = _.isUndefined(proxy.hostname) ? 'localhost' : proxy.hostname
proxy.port = _.isUndefined(proxy.port) ? 9875 : proxy.port

// force protocol to end with ':'
proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
if (proxy.protocol.match(/https?:/) === null) {
log.warn('"%s" is not a supported upstream proxy protocol, defaulting to "http:"',
proxy.protocol)
proxy.protocol = 'http:'
}
}

// force protocol to end with ':'
config.protocol = (config.protocol || 'http').split(':')[0] + ':'
if (config.protocol.match(/https?:/) === null) {
Expand Down Expand Up @@ -276,6 +306,7 @@ var Config = function () {
this.proxyValidateSSL = true
this.preprocessors = {}
this.urlRoot = '/'
this.upstreamProxy = undefined
this.reportSlowerThan = 0
this.loggers = [constant.CONSOLE_APPENDER]
this.transports = ['polling', 'websocket']
Expand Down
11 changes: 9 additions & 2 deletions lib/launcher.js
Expand Up @@ -39,9 +39,15 @@ var Launcher = function (server, emitter, injector) {
return null
}

this.launchSingle = function (protocol, hostname, port, urlRoot) {
this.launchSingle = function (protocol, hostname, port, urlRoot, upstreamProxy) {
var self = this
return function (name) {
if (upstreamProxy) {
protocol = upstreamProxy.protocol
hostname = upstreamProxy.hostname
port = upstreamProxy.port
urlRoot = upstreamProxy.path + urlRoot.substr(1)
}
var url = protocol + '//' + hostname + ':' + port + urlRoot

var locals = {
Expand Down Expand Up @@ -158,7 +164,8 @@ var Launcher = function (server, emitter, injector) {
'config.protocol',
'config.hostname',
'config.port',
'config.urlRoot'
'config.urlRoot',
'config.upstreamProxy'
]

this.kill = function (id, callback) {
Expand Down
22 changes: 13 additions & 9 deletions lib/middleware/karma.js
Expand Up @@ -35,12 +35,12 @@ var SCRIPT_TYPE = {
'.dart': 'application/dart'
}

var filePathToUrlPath = function (filePath, basePath, urlRoot) {
var filePathToUrlPath = function (filePath, basePath, urlRoot, proxyPath) {
if (filePath.indexOf(basePath) === 0) {
return urlRoot + 'base' + filePath.substr(basePath.length)
return proxyPath + urlRoot.substr(1) + 'base' + filePath.substr(basePath.length)
}

return urlRoot + 'absolute' + filePath
return proxyPath + urlRoot.substr(1) + 'absolute' + filePath
}

var getXUACompatibleMetaElement = function (url) {
Expand Down Expand Up @@ -79,8 +79,10 @@ var createKarmaMiddleware = function (
serveFile,
injector,
basePath,
urlRoot
urlRoot,
upstreamProxy
) {
var proxyPath = upstreamProxy ? upstreamProxy.path : '/'
return function (request, response, next) {
// These config values should be up to date on every request
var client = injector.get('config.client')
Expand All @@ -93,7 +95,7 @@ var createKarmaMiddleware = function (

// redirect /__karma__ to /__karma__ (trailing slash)
if (requestUrl === urlRoot.substr(0, urlRoot.length - 1)) {
response.setHeader('Location', urlRoot)
response.setHeader('Location', proxyPath + urlRoot.substr(1))
response.writeHead(301)
return response.end('MOVED PERMANENTLY')
}
Expand Down Expand Up @@ -122,6 +124,7 @@ var createKarmaMiddleware = function (
return serveStaticFile(requestUrl, requestedRangeHeader, response, function (data) {
return data.replace('%KARMA_URL_ROOT%', urlRoot)
.replace('%KARMA_VERSION%', VERSION)
.replace('%KARMA_PROXY_PATH%', proxyPath)
})
}

Expand Down Expand Up @@ -161,7 +164,7 @@ var createKarmaMiddleware = function (
var fileExt = path.extname(filePath)

if (!file.isUrl) {
filePath = filePathToUrlPath(filePath, basePath, urlRoot)
filePath = filePathToUrlPath(filePath, basePath, urlRoot, proxyPath)

if (requestUrl === '/context.html') {
filePath += '?' + file.sha
Expand Down Expand Up @@ -190,7 +193,7 @@ var createKarmaMiddleware = function (
// TODO(vojta): don't compute if it's not in the template
var mappings = files.served.map(function (file) {
// Windows paths contain backslashes and generate bad IDs if not escaped
var filePath = filePathToUrlPath(file.path, basePath, urlRoot).replace(/\\/g, '\\\\')
var filePath = filePathToUrlPath(file.path, basePath, urlRoot, proxyPath).replace(/\\/g, '\\\\')
// Escape single quotes that might be in the filename -
// double quotes should not be allowed!
filePath = filePath.replace(/'/g, '\\\'')
Expand Down Expand Up @@ -223,7 +226,7 @@ var createKarmaMiddleware = function (
response.writeHead(200)
response.end(JSON.stringify({
files: files.included.map(function (file) {
return filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot)
return filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot, proxyPath)
})
}))
})
Expand All @@ -239,7 +242,8 @@ createKarmaMiddleware.$inject = [
'serveFile',
'injector',
'config.basePath',
'config.urlRoot'
'config.urlRoot',
'config.upstreamProxy'
]

// PUBLIC API
Expand Down
128 changes: 70 additions & 58 deletions test/e2e/steps/core_steps.js
Expand Up @@ -71,69 +71,81 @@ module.exports = function coreSteps () {
})(this))
})

this.When(/^I (run|runOut|start|init|stop) Karma( with log-level ([a-z]+))?$/, function (command, withLogLevel, level, callback) {
this.writeConfigFile(tmpDir, tmpConfigFile, (function (_this) {
return function (err, hash) {
this.When(/^I (run|runOut|start|init|stop) Karma( with log-level ([a-z]+))?( behind a proxy on port ([0-9]*) that prepends '([^']*)' to the base path)?$/, function (command, withLogLevel, level, behindProxy, proxyPort, proxyPath, callback) {
var startProxy = function (done) {
if (behindProxy) {
this.proxy.start(proxyPort, proxyPath, done)
} else {
done()
}
}
startProxy.call(this, (function (_this) {
return function (err) {
if (err) {
return callback.fail(new Error(err))
return callback.fail(err)
}
level = withLogLevel === undefined ? 'warn' : level
var configFile = path.join(tmpDir, hash + '.' + tmpConfigFile)
var runtimePath = path.join(baseDir, 'bin', 'karma')
var execKarma = function (done) {
var cmd = runtimePath + ' ' + command + ' --log-level ' + level + ' ' + configFile + ' ' + additionalArgs

return exec(cmd, {
cwd: baseDir
}, done)
}
var runOut = command === 'runOut'
if (command === 'run' || command === 'runOut') {
_this.child = spawn('' + runtimePath, ['start', '--log-level', 'warn', configFile])
var done = function () {
cleansingNeeded = true
_this.child && _this.child.kill()
callback()
_this.writeConfigFile(tmpDir, tmpConfigFile, function (err, hash) {
if (err) {
return callback.fail(new Error(err))
}
level = withLogLevel === undefined ? 'warn' : level
var configFile = path.join(tmpDir, hash + '.' + tmpConfigFile)
var runtimePath = path.join(baseDir, 'bin', 'karma')
var execKarma = function (done) {
var cmd = runtimePath + ' ' + command + ' --log-level ' + level + ' ' + configFile + ' ' + additionalArgs

return exec(cmd, {
cwd: baseDir
}, done)
}
var runOut = command === 'runOut'
if (command === 'run' || command === 'runOut') {
_this.child = spawn('' + runtimePath, ['start', '--log-level', 'warn', configFile])
var done = function () {
cleansingNeeded = true
_this.child && _this.child.kill()
callback()
}

_this.child.on('error', function (error) {
_this.lastRun.error = error
done()
})

_this.child.stderr.on('data', function (chunk) {
_this.lastRun.stderr += chunk.toString()
})

_this.child.stdout.on('data', function (chunk) {
_this.lastRun.stdout += chunk.toString()
var cmd = runtimePath + ' run ' + configFile + ' ' + additionalArgs

setTimeout(function () {
exec(cmd, {
cwd: baseDir
}, function (error, stdout) {
if (error) {
_this.lastRun.error = error
}
if (runOut) {
_this.lastRun.stdout = stdout
}
done()
})
}, 1000)
})
} else {
execKarma(function (error, stdout, stderr) {
if (error) {
_this.child.on('error', function (error) {
_this.lastRun.error = error
}
_this.lastRun.stdout = stdout
_this.lastRun.stderr = stderr
cleansingNeeded = true
callback()
})
}
done()
})

_this.child.stderr.on('data', function (chunk) {
_this.lastRun.stderr += chunk.toString()
})

_this.child.stdout.on('data', function (chunk) {
_this.lastRun.stdout += chunk.toString()
var cmd = runtimePath + ' run ' + configFile + ' ' + additionalArgs

setTimeout(function () {
exec(cmd, {
cwd: baseDir
}, function (error, stdout) {
if (error) {
_this.lastRun.error = error
}
if (runOut) {
_this.lastRun.stdout = stdout
}
done()
})
}, 1000)
})
} else {
execKarma(function (error, stdout, stderr) {
if (error) {
_this.lastRun.error = error
}
_this.lastRun.stdout = stdout
_this.lastRun.stderr = stderr
cleansingNeeded = true
callback()
})
}
})
}
})(this))
})
Expand Down

0 comments on commit 55755e4

Please sign in to comment.