Skip to content

Commit

Permalink
feat(web-server): Allow Range headers in web server.
Browse files Browse the repository at this point in the history
This does not support multiple-ranges.

Closes #2140
  • Loading branch information
TheModMaker committed Jul 26, 2016
1 parent ca4e2c7 commit a567b6f
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 8 deletions.
8 changes: 8 additions & 0 deletions docs/config/02-files.md
Expand Up @@ -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
54 changes: 51 additions & 3 deletions lib/middleware/common.js
Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions lib/middleware/karma.js
Expand Up @@ -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)) {
Expand All @@ -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))
Expand All @@ -118,15 +119,15 @@ 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)
})
}

// serve the favicon
if (requestUrl === '/favicon.ico') {
return serveStaticFile(requestUrl, response)
return serveStaticFile(requestUrl, requestedRangeHeader, response)
}

// serve context.html - execution context within the iframe
Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion lib/middleware/source_files.js
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions test/unit/middleware/source_files.spec.js
Expand Up @@ -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')
Expand Down

1 comment on commit a567b6f

@skc93
Copy link

@skc93 skc93 commented on a567b6f Oct 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.