147 lines
3.2 KiB
JavaScript
Executable File
147 lines
3.2 KiB
JavaScript
Executable File
const {HttpError} = require('./HttpError');
|
|
const http = require('http');
|
|
const {
|
|
parseAcceptEncoding,
|
|
pickAcceptEncoding,
|
|
writeEncoded,
|
|
} = require('./encoding');
|
|
|
|
const PREF_ENCODINGS = ['gzip', 'deflate', 'identity'];
|
|
|
|
class Server {
|
|
constructor() {
|
|
this.running = false;
|
|
this.handlers = [];
|
|
this.urlMaxLength = 65536;
|
|
this.server = http.createServer(this._handleRequest.bind(this));
|
|
this.log = this.log.bind(this);
|
|
this.logTarget = process.stdout;
|
|
this.shutdownCallbacks = [];
|
|
this.compressionOptions = {
|
|
level: 5,
|
|
memLevel: 9,
|
|
windowBits: 15,
|
|
};
|
|
|
|
this.close = this.close.bind(this);
|
|
}
|
|
|
|
addHandler(handler) {
|
|
this.handlers.push(handler);
|
|
return this;
|
|
}
|
|
|
|
addShutdownHook(callback) {
|
|
this.shutdownCallbacks.push(callback);
|
|
return this;
|
|
}
|
|
|
|
log(message) {
|
|
this.logTarget.write(new Date().toISOString() + ' ' + message + '\n');
|
|
}
|
|
|
|
_handleError(req, res, e) {
|
|
let status = 500;
|
|
let message = 'An internal error occurred';
|
|
if(typeof e === 'object' && e.message) {
|
|
status = e.status || 400;
|
|
message = e.message;
|
|
}
|
|
|
|
res.statusCode = status;
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.end(message + '\n');
|
|
}
|
|
|
|
_makeInfo(req, res) {
|
|
const acceptEncoding = parseAcceptEncoding(
|
|
req.headers['accept-encoding']
|
|
);
|
|
return {
|
|
log: this.log,
|
|
pickEncoding: (opts) => pickAcceptEncoding(
|
|
acceptEncoding,
|
|
opts || PREF_ENCODINGS
|
|
),
|
|
writeEncoded: (encoding, data) => writeEncoded(
|
|
res,
|
|
encoding,
|
|
this.compressionOptions,
|
|
data
|
|
),
|
|
};
|
|
}
|
|
|
|
_handleRequest(req, res) {
|
|
try {
|
|
if(req.url.length > this.urlMaxLength) {
|
|
throw new HttpError(400, 'Request too long');
|
|
}
|
|
const info = this._makeInfo(req, res);
|
|
for(const handler of this.handlers) {
|
|
if(handler.apply(req, res, info)) {
|
|
return;
|
|
}
|
|
}
|
|
throw new HttpError(404, 'Not Found');
|
|
} catch(e) {
|
|
this._handleError(req, res, e);
|
|
}
|
|
}
|
|
|
|
baseurl() {
|
|
return 'http://' + this.hostname + ':' + this.port + '/';
|
|
}
|
|
|
|
listen(port, hostname) {
|
|
if(this.running) {
|
|
throw new Error('Already listening');
|
|
}
|
|
const env = {
|
|
hostname,
|
|
log: this.log,
|
|
port,
|
|
};
|
|
return Promise.all(this.handlers.map((h) => h.begin(env)))
|
|
.then(() => new Promise((resolve) => {
|
|
this.server.listen(port, hostname, () => {
|
|
this.running = true;
|
|
this.port = port;
|
|
this.hostname = hostname;
|
|
process.on('SIGINT', this.close);
|
|
resolve(this);
|
|
});
|
|
}));
|
|
}
|
|
|
|
close() {
|
|
if(!this.running) {
|
|
return Promise.resolve(this);
|
|
}
|
|
this.running = false;
|
|
const env = {
|
|
log: this.log,
|
|
};
|
|
this.logTarget.write('\n'); // Skip line containing Ctrl+C indicator
|
|
this.log('Shutting down...');
|
|
return new Promise((resolve) => this.server.close(() => resolve()))
|
|
.then(() => Promise.all(this.handlers.map((h) => h.close(env))))
|
|
.then(() => {
|
|
this.shutdownCallbacks.forEach((fn) => fn(this));
|
|
process.removeListener('SIGINT', this.close);
|
|
this.log('Shutdown');
|
|
return this;
|
|
});
|
|
}
|
|
|
|
printListeningInfo(target) {
|
|
for(const handler of this.handlers) {
|
|
handler.printInfo(target);
|
|
target.write('\n');
|
|
}
|
|
target.write('Available at ' + this.baseurl() + '\n\n');
|
|
}
|
|
}
|
|
|
|
module.exports = {HttpError, Server};
|