SequenceDiagram/bin/server/Server.js

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};