334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
|
|
import { Route } from './classes.route.js';
|
|
import { Handler } from './classes.handler.js';
|
|
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
|
|
|
|
// export types
|
|
import { setupRobots } from './tools.robots.js';
|
|
import { setupManifest } from './tools.manifest.js';
|
|
import { Sitemap } from './classes.sitemap.js';
|
|
import { Feed } from './classes.feed.js';
|
|
import { type IServerOptions } from '../classes.typedserver.js';
|
|
export type TServerStatus = 'initiated' | 'running' | 'stopped';
|
|
|
|
/**
|
|
* can be used to spawn a server to answer http/https calls
|
|
* for constructor options see [[IServerOptions]]
|
|
*/
|
|
export class Server {
|
|
public httpServer: plugins.http.Server | plugins.https.Server;
|
|
public expressAppInstance: plugins.express.Application;
|
|
public routeObjectMap = new Array<Route>();
|
|
public options: IServerOptions;
|
|
public serverStatus: TServerStatus = 'initiated';
|
|
|
|
public feed: Feed;
|
|
public sitemap: Sitemap;
|
|
|
|
public executeAfterStartFunctions: (() => Promise<void>)[] = [];
|
|
|
|
// do stuff when server is ready
|
|
private startedDeferred = plugins.smartpromise.defer();
|
|
// tslint:disable-next-line:member-ordering
|
|
public startedPromise = this.startedDeferred.promise;
|
|
|
|
private socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
|
|
|
constructor(optionsArg: IServerOptions) {
|
|
this.options = {
|
|
...optionsArg,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* allows updating of server options
|
|
* @param optionsArg
|
|
*/
|
|
public updateServerOptions(optionsArg: IServerOptions) {
|
|
Object.assign(this.options, optionsArg);
|
|
}
|
|
|
|
public addTypedRequest(typedrouter: plugins.typedrequest.TypedRouter) {
|
|
this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter));
|
|
}
|
|
|
|
public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void {
|
|
this.executeAfterStartFunctions.push(async () => {
|
|
plugins.typedsocket.TypedSocket.createServer(typedrouter, this);
|
|
});
|
|
}
|
|
|
|
public addRoute(routeStringArg: string, handlerArg?: Handler) {
|
|
const route = new Route(this, routeStringArg);
|
|
if (handlerArg) {
|
|
route.addHandler(handlerArg);
|
|
}
|
|
this.routeObjectMap.push(route);
|
|
return route;
|
|
}
|
|
|
|
public addRouteBefore(routeStringArg: string, handlerArg?: Handler) {
|
|
const route = new Route(this, routeStringArg);
|
|
if (handlerArg) {
|
|
route.addHandler(handlerArg);
|
|
}
|
|
this.routeObjectMap.unshift(route);
|
|
return route;
|
|
}
|
|
|
|
public async start(portArg: number | string = this.options.port, doListen = true) {
|
|
const done = plugins.smartpromise.defer();
|
|
|
|
if (typeof portArg === 'string') {
|
|
portArg = parseInt(portArg);
|
|
}
|
|
|
|
this.expressAppInstance = plugins.express();
|
|
if (!this.httpServer && (!this.options.privateKey || !this.options.publicKey)) {
|
|
console.log('Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy');
|
|
this.httpServer = plugins.http.createServer(this.expressAppInstance);
|
|
} else if (!this.httpServer) {
|
|
console.log('Got SSL certificate. Using it for the http server');
|
|
this.httpServer = plugins.https.createServer(
|
|
{
|
|
key: this.options.privateKey,
|
|
cert: this.options.publicKey,
|
|
},
|
|
this.expressAppInstance
|
|
);
|
|
} else {
|
|
console.log('Using externally supplied http server');
|
|
}
|
|
this.httpServer.keepAliveTimeout = 600 * 1000;
|
|
this.httpServer.headersTimeout = 20 * 1000;
|
|
|
|
// general request handlling
|
|
this.expressAppInstance.use((req, res, next) => {
|
|
next();
|
|
});
|
|
|
|
// forceSsl
|
|
if (this.options.forceSsl) {
|
|
this.expressAppInstance.set('forceSSLOptions', {
|
|
enable301Redirects: true,
|
|
trustXFPHeader: true,
|
|
sslRequiredMessage: 'SSL Required.',
|
|
});
|
|
this.expressAppInstance.use(plugins.expressForceSsl);
|
|
}
|
|
|
|
// cors
|
|
if (this.options.cors) {
|
|
const cors = plugins.cors({
|
|
allowedHeaders: '*',
|
|
methods: '*',
|
|
origin: '*',
|
|
});
|
|
|
|
this.expressAppInstance.use(cors);
|
|
this.expressAppInstance.options('/*', cors);
|
|
}
|
|
|
|
this.expressAppInstance.use((req, res, next) => {
|
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('SERVEZONE_ROUTE', 'LOSSLESS_ORIGIN_CONTAINER');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Expires', new Date(Date.now()).toUTCString());
|
|
next();
|
|
});
|
|
|
|
// body parsing
|
|
this.expressAppInstance.use(async (req, res, next) => {
|
|
if (req.headers['content-type'] === 'application/json') {
|
|
let data = '';
|
|
req.on('data', chunk => {
|
|
data += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
try {
|
|
req.body = plugins.smartjson.parse(data);
|
|
next();
|
|
} catch (error) {
|
|
res.status(400).send('Invalid JSON');
|
|
}
|
|
});
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
this.expressAppInstance.use(plugins.bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
|
|
|
// robots
|
|
if (this.options.robots && this.options.domain) {
|
|
await setupRobots(this, this.options.domain);
|
|
}
|
|
|
|
// manifest.json
|
|
if (this.options.manifest) {
|
|
await setupManifest(this.expressAppInstance, this.options.manifest);
|
|
}
|
|
|
|
// sitemaps
|
|
if (this.options.sitemap) {
|
|
this.sitemap = new Sitemap(this);
|
|
}
|
|
|
|
if (this.options.feed) {
|
|
// feed
|
|
this.feed = new Feed(this);
|
|
}
|
|
|
|
// appVersion
|
|
if (this.options.appVersion) {
|
|
this.expressAppInstance.use((req, res, next) => {
|
|
res.set('appversion', this.options.appVersion);
|
|
next();
|
|
});
|
|
this.addRoute(
|
|
'/appversion',
|
|
new Handler('GET', async (req, res) => {
|
|
res.write(this.options.appVersion);
|
|
res.end();
|
|
})
|
|
);
|
|
}
|
|
|
|
// set up routes in for express
|
|
await this.routeObjectMap.forEach(async (routeArg) => {
|
|
console.log(
|
|
`"${routeArg.routeString}" maps to ${routeArg.handlerObjectMap.getArray().length} handlers`
|
|
);
|
|
const expressRoute = this.expressAppInstance.route(routeArg.routeString);
|
|
routeArg.handlerObjectMap.forEach(async (handler) => {
|
|
console.log(` -> ${handler.httpMethod}`);
|
|
switch (handler.httpMethod) {
|
|
case 'GET':
|
|
expressRoute.get(handler.handlerFunction);
|
|
return;
|
|
case 'POST':
|
|
expressRoute.post(handler.handlerFunction);
|
|
return;
|
|
case 'PUT':
|
|
expressRoute.put(handler.handlerFunction);
|
|
return;
|
|
case 'ALL':
|
|
expressRoute.all(handler.handlerFunction);
|
|
return;
|
|
case 'DELETE':
|
|
expressRoute.delete(handler.handlerFunction);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
});
|
|
});
|
|
|
|
if (this.options.defaultAnswer) {
|
|
this.expressAppInstance.get('/', async (request, response) => {
|
|
response.send(await this.options.defaultAnswer());
|
|
});
|
|
}
|
|
|
|
this.httpServer.on('connection', (connection: plugins.net.Socket) => {
|
|
this.socketMap.add(connection);
|
|
console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`);
|
|
|
|
const closeListener = () => {
|
|
console.log('connection closed');
|
|
cleanupConnection();
|
|
};
|
|
|
|
const errorListener = () => {
|
|
console.log('connection errored');
|
|
cleanupConnection();
|
|
};
|
|
|
|
const endListener = () => {
|
|
console.log('connection ended');
|
|
cleanupConnection();
|
|
};
|
|
|
|
const timeoutListener = () => {
|
|
console.log('connection timed out');
|
|
cleanupConnection();
|
|
};
|
|
|
|
connection.addListener('close', closeListener);
|
|
connection.addListener('error', errorListener);
|
|
connection.addListener('end', endListener);
|
|
connection.addListener('timeout', timeoutListener);
|
|
|
|
const cleanupConnection = async () => {
|
|
connection.removeListener('close', closeListener);
|
|
connection.removeListener('error', errorListener);
|
|
connection.removeListener('end', endListener);
|
|
connection.removeListener('timeout', timeoutListener);
|
|
|
|
if (this.socketMap.checkForObject(connection)) {
|
|
this.socketMap.remove(connection);
|
|
console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
|
|
await plugins.smartdelay.delayFor(0);
|
|
if (connection.destroyed === false) {
|
|
connection.destroy();
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
// finally listen on a port
|
|
if (doListen) {
|
|
this.httpServer.listen(portArg, '0.0.0.0', () => {
|
|
console.log(`now listening on ${portArg}!`);
|
|
this.startedDeferred.resolve();
|
|
this.serverStatus = 'running';
|
|
done.resolve();
|
|
});
|
|
} else {
|
|
console.log(
|
|
'The server does not listen on a network stack and instead expects to get handed requests by other mechanics'
|
|
);
|
|
}
|
|
await done.promise;
|
|
for (const executeAfterStartFunction of this.executeAfterStartFunctions) {
|
|
await executeAfterStartFunction();
|
|
}
|
|
}
|
|
|
|
public getHttpServer() {
|
|
return this.httpServer;
|
|
}
|
|
|
|
public getExpressAppInstance() {
|
|
return this.expressAppInstance;
|
|
}
|
|
|
|
public async stop() {
|
|
const done = plugins.smartpromise.defer();
|
|
if (this.httpServer) {
|
|
this.httpServer.close(async () => {
|
|
this.serverStatus = 'stopped';
|
|
done.resolve();
|
|
});
|
|
await this.socketMap.forEach(async (socket) => {
|
|
socket.destroy();
|
|
});
|
|
} else {
|
|
throw new Error('There is no Server to be stopped. Have you started it?');
|
|
}
|
|
return await done.promise;
|
|
}
|
|
|
|
/**
|
|
* allows handling requests and responses that come from other
|
|
* @param req
|
|
* @param res
|
|
*/
|
|
public async handleReqRes(req: plugins.express.Request, res: plugins.express.Response) {
|
|
this.expressAppInstance(req, res);
|
|
}
|
|
}
|