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