Server.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import path from 'path';
  2. import fs from 'fs';
  3. import http from 'http';
  4. import { pathToFileURL } from 'url';
  5. import express, { Express } from 'express';
  6. import { Middleware, Route, HttpMethod, HttpHandler } from '../base/http';
  7. import { RouteNotSetException } from '../base/exceptions';
  8. import { IncorrectMethodException } from '../base/exceptions';
  9. import { Logger, Message } from '../base/logger';
  10. import { ServerProperties } from './ServerProperties';
  11. import { i18nLoader, $$ } from '../base/i18n';
  12. import { WebSocketServer, WebSocket as WS } from 'ws';
  13. import { InvalidMiddlewareException } from '../base/exceptions';
  14. import { InvalidRouteException } from '../base/exceptions';
  15. import { ServerNotInitializedException } from '../base/exceptions';
  16. import { WebSocketHandler } from '../base/websocket';
  17. import { Guid } from '../base/util';
  18. import { ExpressJsonMiddleware, ExpressUrlencodedMiddleware, InternalServerErrorHandler, NotFoundErrorHandler, PoweredByHeaderMiddleware, SessionMiddleware, SwaggerUiServeMiddleware, SwaggerUiSetupMiddleware } from '../middlewares';
  19. /** @sealed */
  20. class Server {
  21. private instance: Express;
  22. private httpServer: http.Server;
  23. private readonly port: number;
  24. private readonly host: string;
  25. private readonly logger: Logger;
  26. private i18n: i18nLoader;
  27. private readonly httpHandlers: {[key: string]: HttpHandler};
  28. private readonly wsHandlers: {[url: string]: WebSocketHandler};
  29. private readonly wsServers: {[url: string]: WebSocketServer};
  30. private initialized: boolean;
  31. private readonly i18nPath?: string;
  32. private readonly middlewaresPath?: string;
  33. private readonly routesPath?: string;
  34. private readonly viewEngine?: string;
  35. private readonly viewsPath?: string;
  36. private readonly options?: {[key: string]: any};
  37. private readonly swaggerComponents? : object;
  38. private readonly swaggerSecurity? : object;
  39. private readonly swaggerDocsPath? : string;
  40. private readonly swaggerTitle?: string;
  41. private readonly swaggerDescription?: string;
  42. private readonly swaggerApiVersion?: string;
  43. private readonly swaggerRoute?: string;
  44. private swagger: boolean = false;
  45. private json: boolean = false;
  46. private urlencoded: boolean = false;
  47. public constructor(properties: ServerProperties) {
  48. this.instance = express();
  49. this.httpServer = http.createServer(this.instance);
  50. this.port = properties.port;
  51. this.host = properties.host || `http://localhost:${this.port}`;
  52. this.i18n = i18nLoader.getInstance();
  53. if(properties.locale)
  54. this.i18n.setLocale(properties.locale);
  55. this.logger = new Logger();
  56. this.httpHandlers = {};
  57. this.wsHandlers = properties.wsHandlers || {};
  58. this.wsServers = {};
  59. this.json = properties.json || false;
  60. this.urlencoded = properties.urlencoded || false;
  61. this.i18nPath = properties.i18nPath;
  62. this.middlewaresPath = properties.middlewaresPath;
  63. this.routesPath = properties.routesPath;
  64. this.viewEngine = properties.viewEngine;
  65. this.viewsPath = properties.viewsPath;
  66. this.options = properties.options;
  67. if(properties.swagger) {
  68. this.swaggerDocsPath = properties.swagger?.docsPath;
  69. this.swaggerTitle = properties.swagger?.title || 'API Documentation';
  70. this.swaggerDescription = properties.swagger?.description || 'API Documentation';
  71. this.swaggerApiVersion = properties.swagger?.version ||'1.0.0';
  72. this.swaggerRoute = properties.swagger?.route || '/api-docs';
  73. this.swaggerComponents = properties.swagger?.components;
  74. this.swaggerSecurity = properties.swagger?.security;
  75. this.swagger = true;
  76. }
  77. this.initialized = false;
  78. }
  79. public async init(): Promise<Server> {
  80. if(this.viewEngine)
  81. this.instance.set('view engine', this.viewEngine);
  82. if(this.viewsPath)
  83. this.instance.set('views', this.viewsPath);
  84. this.registerSystemMiddlewares();
  85. await this.postInit();
  86. this.initialized = true;
  87. return this;
  88. }
  89. private async postInit(): Promise<void> {
  90. if(this.i18nPath)
  91. this.i18n.load(this.i18nPath);
  92. if(this.middlewaresPath)
  93. await this.registerMiddlewares(this.middlewaresPath);
  94. if(this.routesPath)
  95. await this.registerRoutes(this.routesPath);
  96. if(this.swaggerDocsPath)
  97. fs.writeFileSync(this.swaggerDocsPath, '');
  98. if(Object.keys(this.wsHandlers).length > 0) {
  99. this.registerWsServers();
  100. this.applyWsHandlers();
  101. }
  102. }
  103. private processHttpHandlers(): void {
  104. const handlers: HttpHandler[] = [];
  105. const middlewareOrders: number[] = [];
  106. for(const key in this.httpHandlers) {
  107. const handler = this.httpHandlers[key];
  108. if(handler instanceof Middleware) {
  109. const order = handler.getOrder();
  110. if(middlewareOrders.includes(order))
  111. throw `Ivalid order number: ${handler.constructor.name}`;
  112. middlewareOrders.push(order);
  113. }
  114. handlers.push(this.httpHandlers[key]);
  115. }
  116. handlers.sort((a, b) => a instanceof Middleware && b instanceof Middleware && (a.getOrder() - b.getOrder()) || (a instanceof Middleware && -1 || (b instanceof Middleware && 1 || 0)));
  117. for(const handler of handlers) {
  118. if(handler instanceof Middleware)
  119. this.addMiddleware(handler);
  120. else if (handler instanceof Route)
  121. this.addRoute(handler);
  122. }
  123. }
  124. public addMiddleware(middleware: Middleware): Server {
  125. if(middleware.getRoute() != null)
  126. this.instance.use(<string>middleware.getRoute(), middleware.getAction());
  127. else
  128. this.instance.use(middleware.getAction());
  129. return this;
  130. }
  131. public addRoute(route: Route): Server {
  132. if(route.getRoute() == null)
  133. throw new RouteNotSetException();
  134. switch(route.getMethod()) {
  135. case HttpMethod.GET:
  136. return this.get(route);
  137. case HttpMethod.POST:
  138. return this.post(route);
  139. default:
  140. throw new IncorrectMethodException();
  141. }
  142. }
  143. private registerRoutesDocumentation(): Server {
  144. if(!this.swaggerDocsPath)
  145. return this;
  146. for(const routeName in this.httpHandlers) {
  147. const route = this.httpHandlers[routeName];
  148. if(!(route instanceof Route))
  149. continue;
  150. if(route.getRoute() == null)
  151. throw new RouteNotSetException();
  152. if(![HttpMethod.GET, HttpMethod.POST].includes(route.getMethod()))
  153. throw new IncorrectMethodException();
  154. const docs = route.getDocumentation();
  155. if(docs.length > 0) {
  156. fs.appendFileSync(this.swaggerDocsPath, `${docs}\n`);
  157. this.logInfo($$('org.crazydoctor.expressts.swagger.routeGenerated', { route: route.getRoute() }));
  158. }
  159. }
  160. return this;
  161. }
  162. public registerWsServers(): Server {
  163. for(const url of Object.keys(this.wsHandlers)) {
  164. const wsServer = this.wsServers[url] = new WebSocketServer({ noServer: true });
  165. const wsHandler = this.wsHandlers[url];
  166. wsServer.on(WebSocketHandler.Event.CONNECTION, (ws: WS) => {
  167. wsHandler.onConnect(ws);
  168. ws.on(WebSocketHandler.Event.MESSAGE, wsHandler.onMessage);
  169. ws.on(WebSocketHandler.Event.ERROR, wsHandler.onError);
  170. ws.on(WebSocketHandler.Event.CLOSE, wsHandler.onClose);
  171. });
  172. }
  173. return this;
  174. }
  175. public applyWsHandlers(): Server {
  176. this.httpServer.on('upgrade', (request, socket, head) => {
  177. const url = request.url?.split('?')[0];
  178. if(url && this.wsHandlers[url] && this.wsServers[url]) {
  179. const wsServer = this.wsServers[url];
  180. wsServer.handleUpgrade(request, socket, head, (ws) => {
  181. wsServer.emit(WebSocketHandler.Event.CONNECTION, ws, request);
  182. });
  183. } else {
  184. socket.destroy();
  185. }
  186. });
  187. return this;
  188. }
  189. public getWsConnections(url: string): Set<WS> | null {
  190. const wsServer = this.wsServers[url];
  191. return wsServer ? wsServer.clients : null;
  192. }
  193. public logInfo(message: string): void {
  194. this.logger.info(message);
  195. }
  196. public logError(message: string): void {
  197. this.logger.error(message);
  198. }
  199. public logWarn(message: string): void {
  200. this.logger.warn(message);
  201. }
  202. public log(message: Message): void {
  203. switch(message.type) {
  204. case 'warn':
  205. return this.logWarn(message.text);
  206. case 'error':
  207. return this.logError(message.text);
  208. default:
  209. return this.logInfo(message.text);
  210. }
  211. }
  212. private get(route: Route): Server {
  213. this.instance.get(<string>route.getRoute(), route.getAction());
  214. return this;
  215. }
  216. private post(route: Route): Server {
  217. this.instance.post(<string>route.getRoute(), route.getAction());
  218. return this;
  219. }
  220. private registerSystemMiddlewares(): Server {
  221. const json = this.json, urlencoded = this.urlencoded, swagger = this.swagger;
  222. json && this.appendHttpHandler(new ExpressJsonMiddleware(this));
  223. urlencoded && this.appendHttpHandler(new ExpressUrlencodedMiddleware(this));
  224. this.appendHttpHandler(new InternalServerErrorHandler(this));
  225. this.appendHttpHandler(new NotFoundErrorHandler(this));
  226. this.appendHttpHandler(new PoweredByHeaderMiddleware(this));
  227. this.appendHttpHandler(new SessionMiddleware(this));
  228. swagger && this.appendHttpHandler(new SwaggerUiServeMiddleware(this));
  229. swagger && this.appendHttpHandler(new SwaggerUiSetupMiddleware(this));
  230. return this;
  231. }
  232. private appendHttpHandler(handler: HttpHandler): void {
  233. let id = Guid.new();
  234. while(this.httpHandlers[id] != null)
  235. id = Guid.new();
  236. this.httpHandlers[id] = handler;
  237. }
  238. public async registerRoutes(dir: string): Promise<Server> {
  239. const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });
  240. for(const file of files) {
  241. if(/\.js$/.test(file)) {
  242. const fileUrl = pathToFileURL(path.join(dir, file)).toString();
  243. const RouteClass = await import(fileUrl).then(mod => mod.default?.default ?? mod.default ?? mod);
  244. if(RouteClass.prototype instanceof Route) {
  245. this.appendHttpHandler(<HttpHandler>new RouteClass(this));
  246. } else throw new InvalidRouteException(fileUrl);
  247. }
  248. }
  249. return this;
  250. }
  251. public async registerMiddlewares(dir: string): Promise<Server> {
  252. const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });
  253. for(const file of files) {
  254. if(/\.js$/.test(file)) {
  255. const fileUrl = pathToFileURL(path.join(dir, file)).toString();
  256. const MiddlewareClass = await import(fileUrl).then(mod => mod.default?.default ?? mod.default ?? mod);
  257. if(MiddlewareClass.prototype instanceof Middleware) {
  258. this.appendHttpHandler(<HttpHandler>new MiddlewareClass(this));
  259. } else throw new InvalidMiddlewareException(fileUrl);
  260. }
  261. }
  262. return this;
  263. }
  264. public getLogger(): Logger {
  265. return this.logger;
  266. }
  267. public i18nLoad(path: string): Server {
  268. this.i18n.load(path);
  269. return this;
  270. }
  271. public getHost(): string {
  272. return this.host;
  273. }
  274. public getOption(key: string): any {
  275. return this.options ? this.options[key] || null : null;
  276. }
  277. public start(callback?: () => any): void {
  278. if(!this.initialized)
  279. throw new ServerNotInitializedException();
  280. this.registerRoutesDocumentation();
  281. this.processHttpHandlers();
  282. const cb = (): void => {
  283. this.logInfo($$('org.crazydoctor.expressts.start', { 'port': this.port }));
  284. if(callback) callback();
  285. };
  286. this.httpServer.listen(this.port, cb);
  287. }
  288. }
  289. export { Server };