diff --git a/docs/config/02-files.md b/docs/config/02-files.md index f8cfe8cdc..d7d267484 100644 --- a/docs/config/02-files.md +++ b/docs/config/02-files.md @@ -126,7 +126,15 @@ proxies: { }, ``` +## Webserver features + +* [Range requests][]. +* In-memory caching of files. +* Watching for updates in the files. +* Proxies to alter file paths. + [glob]: https://github.com/isaacs/node-glob [preprocessors]: preprocessors.html [minimatch]: https://github.com/isaacs/minimatch +[Range requests]: https://en.wikipedia.org/wiki/Byte_serving diff --git a/lib/middleware/common.js b/lib/middleware/common.js index e4960f30c..4a3dea1fb 100644 --- a/lib/middleware/common.js +++ b/lib/middleware/common.js @@ -29,9 +29,47 @@ var serve404 = function (response, path) { var createServeFile = function (fs, directory, config) { var cache = Object.create(null) - return function (filepath, response, transform, content, doNotCache) { + return function (filepath, rangeHeader, response, transform, content, doNotCache) { var responseData + var convertForRangeRequest = function () { + // If the header is invalid, ignore + if (!rangeHeader.startsWith('bytes=')) { + return 200 + } + + responseData = new Buffer(responseData) + + var ranges = rangeHeader.substr(6) + if (ranges.indexOf(',') >= 0) { + // Multiple ranges are not supported. + responseData = new Buffer(0) + return 416 // Requested range not satisfiable + } + var parts = /^([0-9]*)-([0-9]*)$/.exec(ranges) + if (!parts || (!parts[1] && !parts[2])) { + return 200 + } + var start, end + if (parts[1]) { + start = Number(parts[1]) + end = parts[2] ? Number(parts[2]) : responseData.length + } else { + end = responseData.length + start = responseData.length - Number(parts[2]) + } + if (end <= start) { + responseData = new Buffer(0) + return 416 // Requested range not satisfiable + } + + response.setHeader( + 'Content-Range', + 'bytes ' + start + '-' + end + '/' + responseData.length) + responseData = responseData.slice(start, end + 1) + return 206 + } + if (directory) { filepath = directory + filepath } @@ -57,7 +95,12 @@ var createServeFile = function (fs, directory, config) { // call custom transform fn to transform the data responseData = transform && transform(content) || content - response.writeHead(200) + if (rangeHeader) { + var code = convertForRangeRequest() + response.writeHead(code) + } else { + response.writeHead(200) + } log.debug('serving (cached): ' + filepath) return response.end(responseData) @@ -77,7 +120,12 @@ var createServeFile = function (fs, directory, config) { // call custom transform fn to transform the data responseData = transform && transform(data.toString()) || data - response.writeHead(200) + if (rangeHeader) { + var code = convertForRangeRequest() + response.writeHead(code) + } else { + response.writeHead(200) + } log.debug('serving: ' + filepath) return response.end(responseData) diff --git a/lib/middleware/karma.js b/lib/middleware/karma.js index d3de85dfa..d59ba8338 100644 --- a/lib/middleware/karma.js +++ b/lib/middleware/karma.js @@ -89,6 +89,7 @@ var createKarmaMiddleware = function ( var jsVersion = injector.get('config.jsVersion') var requestUrl = request.normalizedUrl.replace(/\?.*/, '') + var requestedRangeHeader = request.headers['range'] // redirect /__karma__ to /__karma__ (trailing slash) if (requestUrl === urlRoot.substr(0, urlRoot.length - 1)) { @@ -107,7 +108,7 @@ var createKarmaMiddleware = function ( // serve client.html if (requestUrl === '/') { - return serveStaticFile('/client.html', response, function (data) { + return serveStaticFile('/client.html', requestedRangeHeader, response, function (data) { return data .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url)) .replace('%X_UA_COMPATIBLE_URL%', getXUACompatibleUrl(request.url)) @@ -118,7 +119,7 @@ var createKarmaMiddleware = function ( var jsFiles = ['/karma.js', '/context.js', '/debug.js'] var isRequestingJsFile = jsFiles.indexOf(requestUrl) !== -1 if (isRequestingJsFile) { - return serveStaticFile(requestUrl, response, function (data) { + return serveStaticFile(requestUrl, requestedRangeHeader, response, function (data) { return data.replace('%KARMA_URL_ROOT%', urlRoot) .replace('%KARMA_VERSION%', VERSION) }) @@ -126,7 +127,7 @@ var createKarmaMiddleware = function ( // serve the favicon if (requestUrl === '/favicon.ico') { - return serveStaticFile(requestUrl, response) + return serveStaticFile(requestUrl, requestedRangeHeader, response) } // serve context.html - execution context within the iframe @@ -152,7 +153,7 @@ var createKarmaMiddleware = function ( requestedFileUrl = requestUrl } - fileServer(requestedFileUrl, response, function (data) { + fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) { common.setNoCacheHeaders(response) var scriptTags = files.included.map(function (file) { diff --git a/lib/middleware/source_files.js b/lib/middleware/source_files.js index c32dacba8..641e01022 100644 --- a/lib/middleware/source_files.js +++ b/lib/middleware/source_files.js @@ -30,9 +30,10 @@ var createSourceFilesMiddleware = function (filesPromise, serveFile, basePath, u return filesPromise.then(function (files) { // TODO(vojta): change served to be a map rather then an array var file = findByPath(files.served, requestedFilePath) + var rangeHeader = request.headers['range'] if (file) { - serveFile(file.contentPath || file.path, response, function () { + serveFile(file.contentPath || file.path, rangeHeader, response, function () { if (/\?\w+/.test(request.url)) { // files with timestamps - cache one year, rely on timestamps common.setHeavyCacheHeaders(response) diff --git a/test/unit/middleware/source_files.spec.js b/test/unit/middleware/source_files.spec.js index 0eb2f8557..46ef73f9a 100644 --- a/test/unit/middleware/source_files.spec.js +++ b/test/unit/middleware/source_files.spec.js @@ -59,6 +59,52 @@ describe('middleware.source_files', function () { return files.resolve({included: [], served: list}) } + describe('Range headers', function () { + beforeEach(function () { + servedFiles([ + new File('/src/some.js') + ]) + }) + + it('allows single explicit ranges', function () { + return request(server) + .get('/absolute/src/some.js') + .set('Range', 'bytes=3-6') + .expect('Content-Range', 'bytes 3-6/9') + .expect(206, 'sour') + }) + + it('allows single range with no end', function () { + return request(server) + .get('/absolute/src/some.js') + .set('Range', 'bytes=3-') + .expect('Content-Range', 'bytes 3-9/9') + .expect(206, 'source') + }) + + it('allows single range with suffix', function () { + return request(server) + .get('/absolute/src/some.js') + .set('Range', 'bytes=-5') + .expect('Content-Range', 'bytes 4-9/9') + .expect(206, 'ource') + }) + + it('doesn\'t support multiple ranges', function () { + return request(server) + .get('/absolute/src/some.js') + .set('Range', 'bytes=0-2,-3') + .expect(416, '') + }) + + it('will return 416', function () { + return request(server) + .get('/absolute/src/some.js') + .set('Range', 'bytes=20-') + .expect(416, '') + }) + }) + it('should serve absolute js source files ignoring timestamp', function () { servedFiles([ new File('/src/some.js')