Commit | Line | Data |
---|---|---|
1f7fa4de JB |
1 | import { IncomingMessage, RequestListener, Server, ServerResponse } from 'http'; |
2 | ||
a0202edc | 3 | import { StatusCodes } from 'http-status-codes'; |
1f7fa4de JB |
4 | |
5 | import BaseError from '../../exception/BaseError'; | |
6 | import { ServerOptions } from '../../types/ConfigurationData'; | |
7 | import { | |
8 | ProcedureName, | |
9 | Protocol, | |
10 | ProtocolResponse, | |
11 | ProtocolVersion, | |
12 | RequestPayload, | |
13 | ResponsePayload, | |
14 | ResponseStatus, | |
15 | } from '../../types/UIProtocol'; | |
16 | import Configuration from '../../utils/Configuration'; | |
17 | import logger from '../../utils/Logger'; | |
18 | import Utils from '../../utils/Utils'; | |
19 | import { AbstractUIServer } from './AbstractUIServer'; | |
20 | import UIServiceFactory from './ui-services/UIServiceFactory'; | |
21 | import { UIServiceUtils } from './ui-services/UIServiceUtils'; | |
22 | ||
23 | const moduleName = 'UIHttpServer'; | |
24 | ||
25 | type responseHandler = { procedureName: ProcedureName; res: ServerResponse }; | |
26 | ||
27 | export default class UIHttpServer extends AbstractUIServer { | |
28 | private readonly responseHandlers: Map<string, responseHandler>; | |
29 | ||
30 | public constructor(private options?: ServerOptions) { | |
31 | super(); | |
32 | this.server = new Server(this.requestListener.bind(this) as RequestListener); | |
33 | this.responseHandlers = new Map<string, responseHandler>(); | |
34 | } | |
35 | ||
36 | public start(): void { | |
37 | (this.server as Server).listen(this.options ?? Configuration.getUIServer().options); | |
38 | } | |
39 | ||
40 | public stop(): void { | |
41 | this.chargingStations.clear(); | |
42 | } | |
43 | ||
44 | // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
45 | public sendRequest(request: string): void { | |
46 | // This is intentionally left blank | |
47 | } | |
48 | ||
49 | public sendResponse(response: string): void { | |
50 | const [uuid, payload] = JSON.parse(response) as ProtocolResponse; | |
a0202edc | 51 | let statusCode: StatusCodes; |
1f7fa4de JB |
52 | switch (payload.status) { |
53 | case ResponseStatus.SUCCESS: | |
54 | statusCode = StatusCodes.OK; | |
55 | break; | |
56 | case ResponseStatus.FAILURE: | |
57 | default: | |
58 | statusCode = StatusCodes.BAD_REQUEST; | |
59 | break; | |
60 | } | |
61 | if (this.responseHandlers.has(uuid)) { | |
62 | const { procedureName, res } = this.responseHandlers.get(uuid); | |
63 | res.writeHead(statusCode, { 'Content-Type': 'application/json' }); | |
64 | res.write(JSON.stringify(payload)); | |
65 | res.end(); | |
66 | this.responseHandlers.delete(uuid); | |
67 | } else { | |
68 | logger.error( | |
69 | `${this.logPrefix()} ${moduleName}.sendResponse: Response received for unknown request: ${response}` | |
70 | ); | |
71 | } | |
72 | } | |
73 | ||
74 | public logPrefix(modName?: string, methodName?: string): string { | |
75 | const logMsg = | |
76 | modName && methodName ? ` UI HTTP Server | ${modName}.${methodName}:` : ' UI HTTP Server |'; | |
77 | return Utils.logPrefix(logMsg); | |
78 | } | |
79 | ||
80 | private requestListener(req: IncomingMessage, res: ServerResponse): void { | |
81 | // Expected request URL pathname: /ui/:version/:procedureName | |
82 | const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [ | |
83 | Protocol, | |
84 | ProtocolVersion, | |
85 | ProcedureName | |
86 | ]; | |
87 | const uuid = Utils.generateUUID(); | |
88 | this.responseHandlers.set(uuid, { procedureName, res }); | |
89 | try { | |
90 | if (UIServiceUtils.isProtocolSupported(protocol, version) === false) { | |
91 | throw new BaseError(`Unsupported UI protocol version: '/${protocol}/${version}'`); | |
92 | } | |
93 | req.on('error', (error) => { | |
94 | logger.error( | |
95 | `${this.logPrefix( | |
96 | moduleName, | |
97 | 'requestListener.req.onerror' | |
98 | )} Error at incoming request handling:`, | |
99 | error | |
100 | ); | |
101 | }); | |
102 | if (!this.uiServices.has(version)) { | |
103 | this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this)); | |
104 | } | |
105 | if (req.method === 'POST') { | |
106 | const bodyBuffer = []; | |
107 | let body: RequestPayload; | |
108 | req | |
109 | .on('data', (chunk) => { | |
110 | bodyBuffer.push(chunk); | |
111 | }) | |
112 | .on('end', () => { | |
113 | body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload; | |
114 | this.uiServices | |
115 | .get(version) | |
116 | .requestHandler(this.buildRequest(uuid, procedureName, body ?? {})) | |
117 | .catch(() => { | |
118 | this.sendResponse(this.buildResponse(uuid, { status: ResponseStatus.FAILURE })); | |
119 | }); | |
120 | }); | |
121 | } else { | |
122 | throw new BaseError(`Unsupported HTTP method: '${req.method}'`); | |
123 | } | |
124 | } catch (error) { | |
125 | this.sendResponse(this.buildResponse(uuid, { status: ResponseStatus.FAILURE })); | |
126 | } | |
127 | } | |
128 | ||
129 | private buildRequest( | |
130 | id: string, | |
131 | procedureName: ProcedureName, | |
132 | requestPayload: RequestPayload | |
133 | ): string { | |
134 | return JSON.stringify([id, procedureName, requestPayload]); | |
135 | } | |
136 | ||
137 | private buildResponse(id: string, responsePayload: ResponsePayload): string { | |
138 | return JSON.stringify([id, responsePayload]); | |
139 | } | |
140 | } |