@jnode/server
Version:
Simple web server package for Node.js.
237 lines (211 loc) • 9.97 kB
JavaScript
/*
@jnode/server/server.js
v2
Simple web server package for Node.js.
by JustApple
*/
// dependencies
const http = require('http');
const https = require('https');
const http2 = require('http2');
const stream = require('stream');
const EventEmitter = require('events');
const util = require('./util.js');
// server class
class Server extends EventEmitter {
constructor(router, options = {}) {
super();
this.router = router;
this.options = options;
// start a server
this.server = options.enableHTTP2 ?
((options.key && options.cert) ?
http2.createSecureServer(options) :
http2.createServer(options)) :
((options.key && options.cert) ?
https.createServer(options) :
http.createServer(options));
// request listener
// HTTP/2 also use the same event with the node:http2 Compatibility API
this.server.on('request', async (req, res) => {
let url;
try {
url = new URL(req.url, `http${options.key && options.cert ? 's' : ''}://${req.headers.host || req.headers[':authority']}`);
} catch (e) {
this.emit('warn', e);
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('400 Bad Request', 'utf8');
return;
}
// the context object, mainly for handlers
const ctx = {
req, res, url,
server: this,
path: url.pathname,
host: url.hostname,
method: req.method,
headers: req.headers,
identity: { address: req.socket.remoteAddress, port: req.socket.remotePort },
params: Object.fromEntries(url.searchParams.entries()),
body: req,
cookie: req.headers.cookie ? (() => {
const arr = req.headers.cookie.split('; ');
const cookies = {};
for (let i of arr) {
const equal = i.indexOf('=');
cookies[i.slice(0, equal)] = i.slice(equal + 1);
}
return cookies;
})() : {},
receiveBody: function (max) { return stream.isReadable(this.body) ? util.receiveBody(this.body, max) : this.body },
setCookie: function (key, value, options) { return util.setCookie(this.res, key, value, options); }
};
// the environment object, mainly for routers
const env = {
path: url.pathname.split('/').slice(1).map((i) => { try { return decodeURIComponent(i); } catch { return i; } }),
pathPointer: 0,
host: url.hostname.split('.').filter(Boolean).reverse(),
hostPointer: 0,
codeHandlers: this.options.codeHandlers || {},
i: 0
};
let handler;
try {
handler = await Server.route(this.router, env, ctx, this.options);
if (handler === undefined || handler === null) handler = 404;
} catch (e) { // error while routing
this.emit('e', e, env, ctx);
env.error = e;
handler = 500;
}
await Server.handle(handler, env, ctx, this.options);
});
}
// route
static async route(router, env = {}, ctx = {}, options = {}) {
let r = router;
if (!env.i) env.i = 0;
while (typeof r?.route === 'function') {
env.i++;
if (env.i > (options.maxRoutingSteps || 50)) return 508; // 508 Loop Detected
r = await r.route(env, ctx);
}
return r;
}
// handle
static async handle(handler, env = {}, ctx = {}, options = {}) {
try {
if (typeof handler?.handle === 'function') { // handler
await handler.handle(ctx, env);
} else if (typeof handler === 'function') { // function
await handler(ctx, env);
} else if (typeof handler === 'string') { // string
const data = Buffer.from(handler, 'utf8');
ctx.res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length
});
ctx.res.end(data);
} else if (handler instanceof Uint8Array) { // buffer
ctx.res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Length': handler.length
});
ctx.res.end(handler);
} else if (stream.isReadable(handler)) { // stream
ctx.res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
await stream.promises.pipeline(handler, ctx.res);
} else if (typeof handler === 'number') { // status code
const code = handler;
handler = env.codeHandlers[code] ?? env.codeHandlers['000'];
// handlers without code handlers
if (typeof handler?.handle === 'function') { // handler
await handler.handle(ctx, env, code);
} else if (typeof handler === 'function') { // function
await handler(ctx, env, code);
} else if (typeof handler === 'string') { // string
const data = Buffer.from(handler, 'utf8');
ctx.res.writeHead(code, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length
});
ctx.res.end(data);
} else if (handler instanceof Uint8Array) { // buffer
ctx.res.writeHead(code, {
'Content-Type': 'application/octet-stream',
'Content-Length': handler.length
});
ctx.res.end(handler);
} else if (stream.isReadable(handler)) { // stream
ctx.res.writeHead(code, { 'Content-Type': 'application/octet-stream' });
await stream.promises.pipeline(handler, ctx.res);
} else { // use default status code response
const message = options.showErrorMessage ? (typeof env.error === 'string') ? env.error : env.error?.message ?? '' : '';
try { ctx.res.writeHead(code, { 'Content-Type': 'text/plain; charset=utf-8' }); } catch { }
try { ctx.res.end(`${code} ${http.STATUS_CODES[code] || 'Unknown'}${message ? `\nMessage: ${message}` : ''}`, 'utf8'); } catch { }
}
} else { // invalid handler
throw new Error('JNS: Invalid handler returned from router.');
}
} catch (e) { // error while handling
if (typeof e !== 'number') {
if (!(e?.statusCode)) ctx.server.emit('e', e, env, ctx);
env.error = e;
}
try {
const code = (typeof e === 'number') ? e : e?.statusCode ?? 500;
handler = env.codeHandlers[code] ?? env.codeHandlers['000'];
// handlers without code handlers
if (typeof handler?.handle === 'function') { // handler
await handler.handle(ctx, env, code);
} else if (typeof handler === 'function') { // function
await handler(ctx, env, code);
} else if (typeof handler === 'string') { // string
const data = Buffer.from(handler, 'utf8');
ctx.res.writeHead(code, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length
});
ctx.res.end(data);
} else if (handler instanceof Uint8Array) { // buffer
ctx.res.writeHead(code, {
'Content-Type': 'application/octet-stream',
'Content-Length': handler.length
});
ctx.res.end(handler);
} else if (stream.isReadable(handler)) { // stream
ctx.res.writeHead(code, { 'Content-Type': 'application/octet-stream' });
await stream.promises.pipeline(handler, ctx.res);
} else { // use default status code response
const message = options.showErrorMessage ? (typeof env.error === 'string') ? env.error : env.error?.message ?? '' : '';
ctx.res.writeHead(code, { 'Content-Type': 'text/plain; charset=utf-8' });
ctx.res.end(`${code} ${http.STATUS_CODES[code] || 'Unknown'}${message ? `\nMessage: ${message}` : ''}`, 'utf8');
}
} catch (e) { // error while handling the error while handling :)
ctx.server.emit('warn', e, env, ctx);
const message = options.showErrorMessage ? (typeof env.error === 'string') ? env.error : env.error?.message ?? '' : '';
try { ctx.res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); } catch { }
try { ctx.res.end(`500 Internal Server Error${message ? `\nMessage: ${message}` : ''}`, 'utf8'); } catch { }
}
}
}
listen(...args) {
this.server.listen(...args);
}
close(...args) {
this.server.close(...args);
}
// throw an error event
throw(...args) {
this.emit('e', ...args);
}
}
// create server
function createServer(router, options = {}) {
return new Server(router, options);
}
// export
module.exports = {
Server,
createServer
};