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