mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-19 16:13:06 +00:00
251 lines
7.1 KiB
TypeScript
251 lines
7.1 KiB
TypeScript
/*
|
|
* Copyright (C) 2019-2022 Open StApps
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
import {Converter} from '@openstapps/core-tools';
|
|
import {Logger} from '@openstapps/logger';
|
|
import bodyParser from 'body-parser';
|
|
import express from 'express';
|
|
import * as http from 'http';
|
|
import * as http2 from 'http2';
|
|
import {JSONSchema7} from 'json-schema';
|
|
import morgan from 'morgan';
|
|
import ErrnoException = NodeJS.ErrnoException;
|
|
|
|
/**
|
|
* The Plugin for creating HTTP backend plugins
|
|
*
|
|
* It contains a lot of the boilerplate for creating plugins, and thus simplifies the creation of such.
|
|
* To create your own plugin, you need to extend this class and implement the [[Plugin.onRouteInvoke]] method
|
|
*/
|
|
export abstract class Plugin {
|
|
/**
|
|
* The express instance
|
|
*/
|
|
private readonly app = express();
|
|
|
|
/**
|
|
* The HTTP server
|
|
*/
|
|
private readonly server: http.Server;
|
|
|
|
/**
|
|
* Whether the server is active or not
|
|
*
|
|
* When active is false, it will return 404 on all routes.
|
|
*/
|
|
protected active = false;
|
|
|
|
/**
|
|
* The full URL of the plugin
|
|
*
|
|
* The full URL of the plugin consists out of URL:PORT
|
|
*/
|
|
public get fullUrl() {
|
|
return `${this.url}:${this.port}`;
|
|
}
|
|
|
|
/**
|
|
* The port on which the plugin will listen on
|
|
*/
|
|
public port: string | number | false;
|
|
|
|
/**
|
|
* The schema of the request interfaces defined by the user
|
|
*/
|
|
public readonly requestSchema: JSONSchema7 = {};
|
|
|
|
/**
|
|
* The schema of the response interfaces defined by the user
|
|
*/
|
|
public readonly responseSchema: JSONSchema7 = {};
|
|
|
|
/**
|
|
* Normalize a port into a number, string, or false.
|
|
* @param value the port you want to normalize
|
|
*/
|
|
protected static normalizePort(value: string) {
|
|
const portNumber = Number.parseInt(value, 10);
|
|
|
|
/* istanbul ignore next */
|
|
if (Number.isNaN(portNumber)) {
|
|
// named pipe
|
|
/* istanbul ignore next */
|
|
return value;
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
if (portNumber >= 0) {
|
|
// port number
|
|
return portNumber;
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create an instance of the PluginClient
|
|
*
|
|
* Don't forget to call [[PluginClient.registerPlugin]]!
|
|
* Refer to the examples for how to use the schemas. TODO: examples
|
|
* @param port The port of the plugin
|
|
* @param name The name of the plugin
|
|
* @param url The url of the plugin without the port or anything else, for example `http://localhost`
|
|
* @param route The desired route that will be registered in the backend
|
|
* @param backendUrl The url of the backend
|
|
* @param converter If you want to use an already existing converter, you can pass it here
|
|
* @param requestName the name of the request schema
|
|
* @param responseName the name of the response schema
|
|
* @param version the version. You should retrieve it from the package.json
|
|
*/
|
|
constructor(
|
|
port: number,
|
|
public name: string,
|
|
public url: string,
|
|
public route: string,
|
|
protected backendUrl: string,
|
|
converter: Converter,
|
|
requestName: string,
|
|
responseName: string,
|
|
version: string,
|
|
) {
|
|
this.app.use(bodyParser.json());
|
|
this.port = Plugin.normalizePort(
|
|
/* istanbul ignore next */
|
|
process.env.PORT === undefined ? port.toString() : process.env.PORT,
|
|
);
|
|
this.app.set('port', this.port);
|
|
|
|
// setup express
|
|
this.server = http.createServer(this.app);
|
|
this.server.listen(this.port);
|
|
/* istanbul ignore next */
|
|
this.server.on('error', error => {
|
|
/* istanbul ignore next */
|
|
this.onError(error);
|
|
});
|
|
this.server.on('listening', () => {
|
|
this.onListening();
|
|
});
|
|
|
|
this.requestSchema = converter.getSchema(requestName, version);
|
|
this.responseSchema = converter.getSchema(responseName, version);
|
|
|
|
this.app.use(morgan('dev'));
|
|
|
|
this.app.set('env', process.env.NODE_ENV);
|
|
|
|
this.app.all('*', async (request: express.Request, response: express.Response) => {
|
|
if (this.active) {
|
|
await this.onRouteInvoke(request, response);
|
|
} else {
|
|
response.status(http2.constants.HTTP_STATUS_NOT_FOUND);
|
|
response.send();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Event listener for HTTP server "error" event.
|
|
* @param error The error that occurred
|
|
*/
|
|
|
|
/* istanbul ignore next */
|
|
private onError(error: ErrnoException) {
|
|
if (error.syscall !== 'listen') {
|
|
throw error;
|
|
}
|
|
|
|
const bind = typeof this.port === 'string' ? `Pipe ${this.port}` : `Port ${this.port}`;
|
|
|
|
// handle specific listen errors with friendly messages
|
|
switch (error.code) {
|
|
case 'EACCES': {
|
|
// tslint:disable-next-line:no-floating-promises
|
|
Logger.error(`${bind} requires elevated privileges`);
|
|
process.exit(1);
|
|
break;
|
|
}
|
|
case 'EADDRINUSE': {
|
|
// tslint:disable-next-line:no-floating-promises
|
|
Logger.error(`${bind} is already in use`);
|
|
process.exit(1);
|
|
break;
|
|
}
|
|
default: {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event listener for HTTP server "listening" event.
|
|
*/
|
|
private onListening() {
|
|
const addr = this.server.address();
|
|
/* istanbul ignore next */
|
|
const bind = typeof addr === 'string' ? `pipe ${addr}` : addr === null ? 'null' : `port ${addr.port}`;
|
|
Logger.ok(`Listening on ${bind}`);
|
|
}
|
|
|
|
/**
|
|
* When the route gets invoked
|
|
*
|
|
* Override this method for your own plugin
|
|
* @param request An express Request from the backend
|
|
* @param response An express Response to the backend for you to send back data
|
|
*/
|
|
protected abstract onRouteInvoke(request: express.Request, response: express.Response): Promise<void>;
|
|
|
|
/**
|
|
* Closes the server
|
|
*
|
|
* This will stop the plugin from listening to any requests at all, and is currently an irreversible process.
|
|
* This means, that the instantiated plugin is basically useless afterwards.
|
|
*/
|
|
public async close() {
|
|
return new Promise((resolve, reject) => {
|
|
this.server.close(error => {
|
|
/* istanbul ignore next */
|
|
if (error !== undefined) {
|
|
/* istanbul ignore next */
|
|
reject(error);
|
|
}
|
|
resolve(undefined);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start the plugin
|
|
*
|
|
* **THIS METHOD GETS CALLED AUTOMATICALLY WITH [[PluginClient.registerPlugin]]**
|
|
* If the plugin is not started, it will return 404 on any route
|
|
*/
|
|
public start() {
|
|
this.active = true;
|
|
}
|
|
|
|
/**
|
|
* Stop the plugin
|
|
*
|
|
* **THIS METHOD GETS CALLED AUTOMATICALLY WITH [[PluginClient.unregisterPlugin]]**
|
|
* If the plugin is not started, it will return 404 on any route
|
|
*/
|
|
public stop() {
|
|
// You can't unregister routes from express. So this is a workaround.
|
|
this.active = false;
|
|
}
|
|
}
|