You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
8.7 KiB
298 lines
8.7 KiB
/* |
|
* Copyright 2014 Mozilla Foundation |
|
* |
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
* you may not use this file except in compliance with the License. |
|
* You may obtain a copy of the License at |
|
* |
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
* |
|
* Unless required by applicable law or agreed to in writing, software |
|
* distributed under the License is distributed on an "AS IS" BASIS, |
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
* See the License for the specific language governing permissions and |
|
* limitations under the License. |
|
*/ |
|
/* eslint-disable object-shorthand */ |
|
|
|
'use strict'; |
|
|
|
var http = require('http'); |
|
var path = require('path'); |
|
var fs = require('fs'); |
|
|
|
var mimeTypes = { |
|
'.css': 'text/css', |
|
'.html': 'text/html', |
|
'.js': 'application/javascript', |
|
'.json': 'application/json', |
|
'.svg': 'image/svg+xml', |
|
'.pdf': 'application/pdf', |
|
'.xhtml': 'application/xhtml+xml', |
|
'.gif': 'image/gif', |
|
'.ico': 'image/x-icon', |
|
'.png': 'image/png', |
|
'.log': 'text/plain', |
|
'.bcmap': 'application/octet-stream', |
|
'.properties': 'text/plain', |
|
}; |
|
|
|
var defaultMimeType = 'application/octet-stream'; |
|
|
|
function WebServer() { |
|
this.root = '.'; |
|
this.host = 'localhost'; |
|
this.port = 0; |
|
this.server = null; |
|
this.verbose = false; |
|
this.cacheExpirationTime = 0; |
|
this.disableRangeRequests = false; |
|
this.hooks = { |
|
'GET': [], |
|
'POST': [], |
|
}; |
|
} |
|
WebServer.prototype = { |
|
start: function (callback) { |
|
this._ensureNonZeroPort(); |
|
this.server = http.createServer(this._handler.bind(this)); |
|
this.server.listen(this.port, this.host, callback); |
|
console.log( |
|
'Server running at http://' + this.host + ':' + this.port + '/'); |
|
}, |
|
stop: function (callback) { |
|
this.server.close(callback); |
|
this.server = null; |
|
}, |
|
_ensureNonZeroPort: function () { |
|
if (!this.port) { |
|
// If port is 0, a random port will be chosen instead. Do not set a host |
|
// name to make sure that the port is synchronously set by .listen(). |
|
var server = http.createServer().listen(0); |
|
var address = server.address(); |
|
// .address().port being available synchronously is merely an |
|
// implementation detail. So we are defensive here and fall back to some |
|
// fixed port when the address is not available yet. |
|
this.port = address ? address.port : 8000; |
|
server.close(); |
|
} |
|
}, |
|
_handler: function (req, res) { |
|
var url = req.url.replace(/\/\//g, '/'); |
|
var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url); |
|
var pathPart = decodeURI(urlParts[1]), queryPart = urlParts[3]; |
|
var verbose = this.verbose; |
|
|
|
var methodHooks = this.hooks[req.method]; |
|
if (!methodHooks) { |
|
res.writeHead(405); |
|
res.end('Unsupported request method', 'utf8'); |
|
return; |
|
} |
|
var handled = methodHooks.some(function (hook) { |
|
return hook(req, res); |
|
}); |
|
if (handled) { |
|
return; |
|
} |
|
|
|
if (pathPart === '/favicon.ico') { |
|
fs.realpath(path.join(this.root, 'test/resources/favicon.ico'), |
|
checkFile); |
|
return; |
|
} |
|
|
|
var disableRangeRequests = this.disableRangeRequests; |
|
var cacheExpirationTime = this.cacheExpirationTime; |
|
|
|
var filePath; |
|
fs.realpath(path.join(this.root, pathPart), checkFile); |
|
|
|
function checkFile(err, file) { |
|
if (err) { |
|
res.writeHead(404); |
|
res.end(); |
|
if (verbose) { |
|
console.error(url + ': not found'); |
|
} |
|
return; |
|
} |
|
filePath = file; |
|
fs.stat(filePath, statFile); |
|
} |
|
|
|
var fileSize; |
|
|
|
function statFile(err, stats) { |
|
if (err) { |
|
res.writeHead(500); |
|
res.end(); |
|
return; |
|
} |
|
|
|
fileSize = stats.size; |
|
var isDir = stats.isDirectory(); |
|
if (isDir && !/\/$/.test(pathPart)) { |
|
res.setHeader('Location', pathPart + '/' + urlParts[2]); |
|
res.writeHead(301); |
|
res.end('Redirected', 'utf8'); |
|
return; |
|
} |
|
if (isDir) { |
|
serveDirectoryIndex(filePath); |
|
return; |
|
} |
|
|
|
var range = req.headers['range']; |
|
if (range && !disableRangeRequests) { |
|
var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range); |
|
if (!rangesMatches) { |
|
res.writeHead(501); |
|
res.end('Bad range', 'utf8'); |
|
if (verbose) { |
|
console.error(url + ': bad range: "' + range + '"'); |
|
} |
|
return; |
|
} |
|
var start = +rangesMatches[1]; |
|
var end = +rangesMatches[2]; |
|
if (verbose) { |
|
console.log(url + ': range ' + start + ' - ' + end); |
|
} |
|
serveRequestedFileRange(filePath, |
|
start, |
|
isNaN(end) ? fileSize : (end + 1)); |
|
return; |
|
} |
|
if (verbose) { |
|
console.log(url); |
|
} |
|
serveRequestedFile(filePath); |
|
} |
|
|
|
function escapeHTML(untrusted) { |
|
// Escape untrusted input so that it can safely be used in a HTML response |
|
// in HTML and in HTML attributes. |
|
return untrusted |
|
.replace(/&/g, '&') |
|
.replace(/</g, '<') |
|
.replace(/>/g, '>') |
|
.replace(/"/g, '"') |
|
.replace(/'/g, '''); |
|
} |
|
|
|
function serveDirectoryIndex(dir) { |
|
res.setHeader('Content-Type', 'text/html'); |
|
res.writeHead(200); |
|
|
|
if (queryPart === 'frame') { |
|
res.end('<html><frameset cols=*,200><frame name=pdf>' + |
|
'<frame src=\"' + encodeURI(pathPart) + |
|
'?side\"></frameset></html>', 'utf8'); |
|
return; |
|
} |
|
var all = queryPart === 'all'; |
|
fs.readdir(dir, function (err, files) { |
|
if (err) { |
|
res.end(); |
|
return; |
|
} |
|
res.write('<html><head><meta charset=\"utf-8\"></head><body>' + |
|
'<h1>PDFs of ' + pathPart + '</h1>\n'); |
|
if (pathPart !== '/') { |
|
res.write('<a href=\"..\">..</a><br>\n'); |
|
} |
|
files.forEach(function (file) { |
|
var stat; |
|
var item = pathPart + file; |
|
var href = ''; |
|
var label = ''; |
|
var extraAttributes = ''; |
|
try { |
|
stat = fs.statSync(path.join(dir, file)); |
|
} catch (e) { |
|
href = encodeURI(item); |
|
label = file + ' (' + e + ')'; |
|
extraAttributes = ' style="color:red"'; |
|
} |
|
if (stat) { |
|
if (stat.isDirectory()) { |
|
href = encodeURI(item); |
|
label = file; |
|
} else if (path.extname(file).toLowerCase() === '.pdf') { |
|
href = '/web/viewer.html?file=' + encodeURIComponent(item); |
|
label = file; |
|
extraAttributes = ' target="pdf"'; |
|
} else if (all) { |
|
href = encodeURI(item); |
|
label = file; |
|
} |
|
} |
|
if (label) { |
|
res.write('<a href=\"' + escapeHTML(href) + '\"' + |
|
extraAttributes + '>' + escapeHTML(label) + '</a><br>\n'); |
|
} |
|
}); |
|
if (files.length === 0) { |
|
res.write('<p>no files found</p>\n'); |
|
} |
|
if (!all && queryPart !== 'side') { |
|
res.write('<hr><p>(only PDF files are shown, ' + |
|
'<a href=\"?all\">show all</a>)</p>\n'); |
|
} |
|
res.end('</body></html>'); |
|
}); |
|
} |
|
|
|
function serveRequestedFile(filePath) { |
|
var stream = fs.createReadStream(filePath, { flags: 'rs', }); |
|
|
|
stream.on('error', function (error) { |
|
res.writeHead(500); |
|
res.end(); |
|
}); |
|
|
|
var ext = path.extname(filePath).toLowerCase(); |
|
var contentType = mimeTypes[ext] || defaultMimeType; |
|
|
|
if (!disableRangeRequests) { |
|
res.setHeader('Accept-Ranges', 'bytes'); |
|
} |
|
res.setHeader('Content-Type', contentType); |
|
res.setHeader('Content-Length', fileSize); |
|
if (cacheExpirationTime > 0) { |
|
var expireTime = new Date(); |
|
expireTime.setSeconds(expireTime.getSeconds() + cacheExpirationTime); |
|
res.setHeader('Expires', expireTime.toUTCString()); |
|
} |
|
res.writeHead(200); |
|
|
|
stream.pipe(res); |
|
} |
|
|
|
function serveRequestedFileRange(filePath, start, end) { |
|
var stream = fs.createReadStream(filePath, { |
|
flags: 'rs', start: start, end: end - 1, }); |
|
|
|
stream.on('error', function (error) { |
|
res.writeHead(500); |
|
res.end(); |
|
}); |
|
|
|
var ext = path.extname(filePath).toLowerCase(); |
|
var contentType = mimeTypes[ext] || defaultMimeType; |
|
|
|
res.setHeader('Accept-Ranges', 'bytes'); |
|
res.setHeader('Content-Type', contentType); |
|
res.setHeader('Content-Length', (end - start)); |
|
res.setHeader('Content-Range', |
|
'bytes ' + start + '-' + (end - 1) + '/' + fileSize); |
|
res.writeHead(206); |
|
|
|
stream.pipe(res); |
|
} |
|
|
|
}, |
|
}; |
|
|
|
exports.WebServer = WebServer;
|
|
|