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