build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / UIHttpServer.ts
1 import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http';
2
3 import { StatusCodes } from 'http-status-codes';
4
5 import { AbstractUIServer } from './AbstractUIServer';
6 import { UIServerUtils } from './UIServerUtils';
7 import { BaseError } from '../../exception';
8 import {
9 type ProcedureName,
10 type Protocol,
11 type ProtocolRequest,
12 type ProtocolResponse,
13 type ProtocolVersion,
14 type RequestPayload,
15 ResponseStatus,
16 type UIServerConfiguration,
17 } from '../../types';
18 import {
19 Constants,
20 generateUUID,
21 isNotEmptyString,
22 isNullOrUndefined,
23 logPrefix,
24 logger,
25 } from '../../utils';
26
27 const moduleName = 'UIHttpServer';
28
29 enum HttpMethods {
30 GET = 'GET',
31 PUT = 'PUT',
32 POST = 'POST',
33 PATCH = 'PATCH',
34 }
35
36 export class UIHttpServer extends AbstractUIServer {
37 public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) {
38 super(uiServerConfiguration);
39 }
40
41 public start(): void {
42 this.httpServer.on('request', this.requestListener.bind(this) as RequestListener);
43 this.startHttpServer();
44 }
45
46 // eslint-disable-next-line @typescript-eslint/no-unused-vars
47 public sendRequest(request: ProtocolRequest): void {
48 // This is intentionally left blank
49 }
50
51 public sendResponse(response: ProtocolResponse): void {
52 const [uuid, payload] = response;
53 try {
54 if (this.hasResponseHandler(uuid) === true) {
55 const res = this.responseHandlers.get(uuid) as ServerResponse;
56 res
57 .writeHead(this.responseStatusToStatusCode(payload.status), {
58 'Content-Type': 'application/json',
59 })
60 .end(JSON.stringify(payload));
61 } else {
62 logger.error(
63 `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}`,
64 );
65 }
66 } catch (error) {
67 logger.error(
68 `${this.logPrefix(moduleName, 'sendResponse')} Error at sending response id '${uuid}':`,
69 error,
70 );
71 } finally {
72 this.responseHandlers.delete(uuid);
73 }
74 }
75
76 public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => {
77 const logMsgPrefix = prefixSuffix ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server';
78 const logMsg =
79 isNotEmptyString(modName) && isNotEmptyString(methodName)
80 ? ` ${logMsgPrefix} | ${modName}.${methodName}:`
81 : ` ${logMsgPrefix} |`;
82 return logPrefix(logMsg);
83 };
84
85 private requestListener(req: IncomingMessage, res: ServerResponse): void {
86 this.authenticate(req, (err) => {
87 if (err) {
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();
95 req.destroy();
96 }
97 });
98 // Expected request URL pathname: /ui/:version/:procedureName
99 const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [
100 Protocol,
101 ProtocolVersion,
102 ProcedureName,
103 ];
104 const uuid = generateUUID();
105 this.responseHandlers.set(uuid, res);
106 try {
107 const fullProtocol = `${protocol}${version}`;
108 if (UIServerUtils.isProtocolAndVersionSupported(fullProtocol) === false) {
109 throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`);
110 }
111 this.registerProtocolVersionUIService(version);
112 req.on('error', (error) => {
113 logger.error(
114 `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`,
115 error,
116 );
117 });
118 if (req.method === HttpMethods.POST) {
119 const bodyBuffer: Uint8Array[] = [];
120 req
121 .on('data', (chunk: Uint8Array) => {
122 bodyBuffer.push(chunk);
123 })
124 .on('end', () => {
125 const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload;
126 this.uiServices
127 .get(version)
128 ?.requestHandler(
129 this.buildProtocolRequest(
130 uuid,
131 procedureName,
132 body ?? Constants.EMPTY_FREEZED_OBJECT,
133 ),
134 )
135 .then((protocolResponse: ProtocolResponse) => {
136 if (!isNullOrUndefined(protocolResponse)) {
137 this.sendResponse(protocolResponse);
138 }
139 })
140 .catch(Constants.EMPTY_FUNCTION);
141 });
142 } else {
143 throw new BaseError(`Unsupported HTTP method: '${req.method}'`);
144 }
145 } catch (error) {
146 logger.error(
147 `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`,
148 error,
149 );
150 this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }));
151 }
152 }
153
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 }
164 }