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