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'; | |
eb3abc4f | 6 | import type { UIServerConfiguration } 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'; | |
1f7fa4de JB |
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 | ||
eb3abc4f JB |
29 | public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) { |
30 | super(uiServerConfiguration); | |
31 | this.httpServer = new Server(this.requestListener.bind(this) as RequestListener); | |
1f7fa4de JB |
32 | this.responseHandlers = new Map<string, responseHandler>(); |
33 | } | |
34 | ||
35 | public start(): void { | |
eb3abc4f JB |
36 | if (this.httpServer.listening === false) { |
37 | this.httpServer.listen(this.uiServerConfiguration.options); | |
10d244c0 | 38 | } |
1f7fa4de JB |
39 | } |
40 | ||
41 | public stop(): void { | |
42 | this.chargingStations.clear(); | |
43 | } | |
44 | ||
45 | // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
5e3cb728 | 46 | public sendRequest(request: ProtocolRequest): void { |
1f7fa4de JB |
47 | // This is intentionally left blank |
48 | } | |
49 | ||
5e3cb728 JB |
50 | public sendResponse(response: ProtocolResponse): void { |
51 | const [uuid, payload] = response; | |
771633ff | 52 | const statusCode = this.responseStatusToStatusCode(payload.status); |
10d244c0 | 53 | if (this.responseHandlers.has(uuid) === true) { |
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( | |
5e3cb728 | 61 | `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}` |
1f7fa4de JB |
62 | ); |
63 | } | |
64 | } | |
65 | ||
0d2cec76 JB |
66 | public logPrefix(modName?: string, methodName?: string, prefixSuffix?: string): string { |
67 | const logMsgPrefix = prefixSuffix ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server'; | |
1f7fa4de | 68 | const logMsg = |
0d2cec76 | 69 | modName && methodName ? ` ${logMsgPrefix} | ${modName}.${methodName}:` : ` ${logMsgPrefix} |`; |
1f7fa4de JB |
70 | return Utils.logPrefix(logMsg); |
71 | } | |
72 | ||
73 | private requestListener(req: IncomingMessage, res: ServerResponse): void { | |
eb3abc4f JB |
74 | if (this.isBasicAuthEnabled() === true && this.isValidBasicAuth(req) === false) { |
75 | res.setHeader('Content-Type', 'text/plain'); | |
76 | res.setHeader('WWW-Authenticate', 'Basic realm=users'); | |
77 | res.writeHead(StatusCodes.UNAUTHORIZED); | |
78 | res.end(`${StatusCodes.UNAUTHORIZED} Unauthorized`); | |
79 | return; | |
80 | } | |
1f7fa4de JB |
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 { | |
a92929f1 | 90 | if (UIServiceUtils.isProtocolAndVersionSupported(protocol, version) === false) { |
1f7fa4de JB |
91 | throw new BaseError(`Unsupported UI protocol version: '/${protocol}/${version}'`); |
92 | } | |
93 | req.on('error', (error) => { | |
94 | logger.error( | |
a745e412 | 95 | `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`, |
1f7fa4de JB |
96 | error |
97 | ); | |
98 | }); | |
6d9876e7 | 99 | if (this.uiServices.has(version) === false) { |
1f7fa4de JB |
100 | this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this)); |
101 | } | |
102 | if (req.method === 'POST') { | |
103 | const bodyBuffer = []; | |
1f7fa4de JB |
104 | req |
105 | .on('data', (chunk) => { | |
106 | bodyBuffer.push(chunk); | |
107 | }) | |
108 | .on('end', () => { | |
a745e412 | 109 | const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload; |
1f7fa4de JB |
110 | this.uiServices |
111 | .get(version) | |
852a4c5f | 112 | .requestHandler(this.buildProtocolRequest(uuid, procedureName, body ?? {})) |
1f7fa4de | 113 | .catch(() => { |
852a4c5f JB |
114 | this.sendResponse( |
115 | this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }) | |
116 | ); | |
1f7fa4de JB |
117 | }); |
118 | }); | |
119 | } else { | |
120 | throw new BaseError(`Unsupported HTTP method: '${req.method}'`); | |
121 | } | |
122 | } catch (error) { | |
a745e412 JB |
123 | logger.error( |
124 | `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`, | |
125 | error | |
126 | ); | |
852a4c5f | 127 | this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE })); |
1f7fa4de JB |
128 | } |
129 | } | |
130 | ||
771633ff JB |
131 | private responseStatusToStatusCode(status: ResponseStatus): StatusCodes { |
132 | switch (status) { | |
133 | case ResponseStatus.SUCCESS: | |
134 | return StatusCodes.OK; | |
135 | case ResponseStatus.FAILURE: | |
136 | return StatusCodes.BAD_REQUEST; | |
137 | default: | |
138 | return StatusCodes.INTERNAL_SERVER_ERROR; | |
139 | } | |
140 | } | |
1f7fa4de | 141 | } |