317 lines
9.5 KiB
JavaScript
317 lines
9.5 KiB
JavaScript
/*
|
|
* 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': [crossOriginHandler],
|
|
'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);
|
|
// guard against directory traversal attacks,
|
|
// e.g. /../../../../../../../etc/passwd
|
|
// which let you make GET requests for files outside of this.root
|
|
var pathPart = path.normalize(decodeURI(urlParts[1]));
|
|
var 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);
|
|
}
|
|
|
|
},
|
|
};
|
|
|
|
// This supports the "Cross-origin" test in test/unit/api_spec.js
|
|
// It is here instead of test.js so that when the test will still complete as
|
|
// expected if the user does "gulp server" and then visits
|
|
// http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin
|
|
function crossOriginHandler(req, res) {
|
|
if (req.url === '/test/pdfs/basicapi.pdf?cors=withCredentials') {
|
|
res.setHeader('Access-Control-Allow-Origin', req.headers['origin']);
|
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
}
|
|
if (req.url === '/test/pdfs/basicapi.pdf?cors=withoutCredentials') {
|
|
res.setHeader('Access-Control-Allow-Origin', req.headers['origin']);
|
|
}
|
|
}
|
|
|
|
exports.WebServer = WebServer;
|