build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / UIHttpServer.ts
CommitLineData
01f4001e 1import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http';
1f7fa4de 2
a0202edc 3import { StatusCodes } from 'http-status-codes';
1f7fa4de 4
4c3c0d59
JB
5import { AbstractUIServer } from './AbstractUIServer';
6import { UIServerUtils } from './UIServerUtils';
268a74bb 7import { BaseError } from '../../exception';
1f7fa4de 8import {
e0b0ee21
JB
9 type ProcedureName,
10 type Protocol,
11 type ProtocolRequest,
12 type ProtocolResponse,
13 type ProtocolVersion,
14 type RequestPayload,
1f7fa4de 15 ResponseStatus,
268a74bb
JB
16 type UIServerConfiguration,
17} from '../../types';
9bf0ef23
JB
18import {
19 Constants,
20 generateUUID,
21 isNotEmptyString,
22 isNullOrUndefined,
23 logPrefix,
24 logger,
25} from '../../utils';
1f7fa4de
JB
26
27const moduleName = 'UIHttpServer';
28
1185579a
JB
29enum HttpMethods {
30 GET = 'GET',
31 PUT = 'PUT',
32 POST = 'POST',
33 PATCH = 'PATCH',
34}
35
268a74bb 36export class UIHttpServer extends AbstractUIServer {
eb3abc4f
JB
37 public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) {
38 super(uiServerConfiguration);
1f7fa4de
JB
39 }
40
41 public start(): void {
daa6505e 42 this.httpServer.on('request', this.requestListener.bind(this) as RequestListener);
a307349b 43 this.startHttpServer();
1f7fa4de
JB
44 }
45
1f7fa4de 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;
976d11ec 53 try {
1ca4a038 54 if (this.hasResponseHandler(uuid) === true) {
976d11ec 55 const res = this.responseHandlers.get(uuid) as ServerResponse;
b2e2c274
JB
56 res
57 .writeHead(this.responseStatusToStatusCode(payload.status), {
58 'Content-Type': 'application/json',
59 })
60 .end(JSON.stringify(payload));
976d11ec
JB
61 } else {
62 logger.error(
5edd8ba0 63 `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}`,
976d11ec
JB
64 );
65 }
66 } catch (error) {
1f7fa4de 67 logger.error(
976d11ec 68 `${this.logPrefix(moduleName, 'sendResponse')} Error at sending response id '${uuid}':`,
5edd8ba0 69 error,
1f7fa4de 70 );
e2c77f10
JB
71 } finally {
72 this.responseHandlers.delete(uuid);
1f7fa4de
JB
73 }
74 }
75
8b7072dc 76 public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => {
0d2cec76 77 const logMsgPrefix = prefixSuffix ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server';
1f7fa4de 78 const logMsg =
9bf0ef23 79 isNotEmptyString(modName) && isNotEmptyString(methodName)
1b271a54
JB
80 ? ` ${logMsgPrefix} | ${modName}.${methodName}:`
81 : ` ${logMsgPrefix} |`;
9bf0ef23 82 return logPrefix(logMsg);
8b7072dc 83 };
1f7fa4de
JB
84
85 private requestListener(req: IncomingMessage, res: ServerResponse): void {
72092cfc 86 this.authenticate(req, (err) => {
623b39b5 87 if (err) {
b2e2c274
JB
88 res
89 .writeHead(StatusCodes.UNAUTHORIZED, {
90 'Content-Type': 'text/plain',
91 'WWW-Authenticate': 'Basic realm=users',
92 })
93 .end(`${StatusCodes.UNAUTHORIZED} Unauthorized`)
94 .destroy();
623b39b5 95 req.destroy();
623b39b5
JB
96 }
97 });
1f7fa4de
JB
98 // Expected request URL pathname: /ui/:version/:procedureName
99 const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [
100 Protocol,
101 ProtocolVersion,
5edd8ba0 102 ProcedureName,
1f7fa4de 103 ];
9bf0ef23 104 const uuid = generateUUID();
94dc3080 105 this.responseHandlers.set(uuid, res);
1f7fa4de 106 try {
7cb5b17f 107 const fullProtocol = `${protocol}${version}`;
ed3d2808 108 if (UIServerUtils.isProtocolAndVersionSupported(fullProtocol) === false) {
7cb5b17f 109 throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`);
1f7fa4de 110 }
daa6505e 111 this.registerProtocolVersionUIService(version);
72092cfc 112 req.on('error', (error) => {
1f7fa4de 113 logger.error(
a745e412 114 `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`,
5edd8ba0 115 error,
1f7fa4de
JB
116 );
117 });
1185579a 118 if (req.method === HttpMethods.POST) {
1f7fa4de 119 const bodyBuffer = [];
1f7fa4de 120 req
72092cfc 121 .on('data', (chunk) => {
1f7fa4de
JB
122 bodyBuffer.push(chunk);
123 })
124 .on('end', () => {
a745e412 125 const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload;
1f7fa4de
JB
126 this.uiServices
127 .get(version)
59b6ed8d
JB
128 ?.requestHandler(
129 this.buildProtocolRequest(
130 uuid,
131 procedureName,
5edd8ba0
JB
132 body ?? Constants.EMPTY_FREEZED_OBJECT,
133 ),
59b6ed8d 134 )
0b22144c 135 .then((protocolResponse: ProtocolResponse) => {
9bf0ef23 136 if (!isNullOrUndefined(protocolResponse)) {
0b22144c
JB
137 this.sendResponse(protocolResponse);
138 }
139 })
59b6ed8d 140 .catch(Constants.EMPTY_FUNCTION);
1f7fa4de
JB
141 });
142 } else {
143 throw new BaseError(`Unsupported HTTP method: '${req.method}'`);
144 }
145 } catch (error) {
a745e412
JB
146 logger.error(
147 `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`,
5edd8ba0 148 error,
a745e412 149 );
852a4c5f 150 this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }));
1f7fa4de
JB
151 }
152 }
153
771633ff
JB
154 private responseStatusToStatusCode(status: ResponseStatus): StatusCodes {
155 switch (status) {
156 case ResponseStatus.SUCCESS:
157 return StatusCodes.OK;
158 case ResponseStatus.FAILURE:
159 return StatusCodes.BAD_REQUEST;
160 default:
161 return StatusCodes.INTERNAL_SERVER_ERROR;
162 }
163 }
1f7fa4de 164}