0b6eee9feb5bb783610a5f9001d4f04f6b7fe008
[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.js'
6 import { UIServerUtils } from './UIServerUtils.js'
7 import { BaseError } from '../../exception/index.js'
8 import {
9 ApplicationProtocolVersion,
10 type ProcedureName,
11 type Protocol,
12 type ProtocolRequest,
13 type ProtocolResponse,
14 type ProtocolVersion,
15 type RequestPayload,
16 ResponseStatus,
17 type UIServerConfiguration
18 } from '../../types/index.js'
19 import { Constants, generateUUID, isNotEmptyString, logPrefix, logger } from '../../utils/index.js'
20
21 const moduleName = 'UIHttpServer'
22
23 enum HttpMethods {
24 GET = 'GET',
25 PUT = 'PUT',
26 POST = 'POST',
27 PATCH = 'PATCH',
28 }
29
30 export class UIHttpServer extends AbstractUIServer {
31 public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
32 super(uiServerConfiguration)
33 }
34
35 public start (): void {
36 this.httpServer.on('request', this.requestListener.bind(this) as RequestListener)
37 this.startHttpServer()
38 }
39
40 public sendRequest (request: ProtocolRequest): void {
41 switch (this.uiServerConfiguration.version) {
42 case ApplicationProtocolVersion.VERSION_20:
43 this.httpServer.emit('request', request)
44 break
45 }
46 }
47
48 public sendResponse (response: ProtocolResponse): void {
49 const [uuid, payload] = response
50 try {
51 if (this.hasResponseHandler(uuid)) {
52 const res = this.responseHandlers.get(uuid) as ServerResponse
53 res
54 .writeHead(this.responseStatusToStatusCode(payload.status), {
55 'Content-Type': 'application/json'
56 })
57 .end(JSON.stringify(payload))
58 } else {
59 logger.error(
60 `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}`
61 )
62 }
63 } catch (error) {
64 logger.error(
65 `${this.logPrefix(moduleName, 'sendResponse')} Error at sending response id '${uuid}':`,
66 error
67 )
68 } finally {
69 this.responseHandlers.delete(uuid)
70 }
71 }
72
73 public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => {
74 const logMsgPrefix = prefixSuffix != null ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server'
75 const logMsg =
76 isNotEmptyString(modName) && isNotEmptyString(methodName)
77 ? ` ${logMsgPrefix} | ${modName}.${methodName}:`
78 : ` ${logMsgPrefix} |`
79 return logPrefix(logMsg)
80 }
81
82 private requestListener (req: IncomingMessage, res: ServerResponse): void {
83 this.authenticate(req, (err) => {
84 if (err != null) {
85 res
86 .writeHead(StatusCodes.UNAUTHORIZED, {
87 'Content-Type': 'text/plain',
88 'WWW-Authenticate': 'Basic realm=users'
89 })
90 .end(`${StatusCodes.UNAUTHORIZED} Unauthorized`)
91 .destroy()
92 req.destroy()
93 }
94 })
95 // Expected request URL pathname: /ui/:version/:procedureName
96 const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [
97 Protocol,
98 ProtocolVersion,
99 ProcedureName,
100 ]
101 const uuid = generateUUID()
102 this.responseHandlers.set(uuid, res)
103 try {
104 const fullProtocol = `${protocol}${version}`
105 if (!UIServerUtils.isProtocolAndVersionSupported(fullProtocol)) {
106 throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`)
107 }
108 this.registerProtocolVersionUIService(version)
109 req.on('error', (error) => {
110 logger.error(
111 `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`,
112 error
113 )
114 })
115 if (req.method === HttpMethods.POST) {
116 const bodyBuffer: Uint8Array[] = []
117 req
118 .on('data', (chunk: Uint8Array) => {
119 bodyBuffer.push(chunk)
120 })
121 .on('end', () => {
122 const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload
123 this.uiServices
124 .get(version)
125 ?.requestHandler(
126 this.buildProtocolRequest(
127 uuid,
128 procedureName,
129 body ?? Constants.EMPTY_FROZEN_OBJECT
130 )
131 )
132 .then((protocolResponse?: ProtocolResponse) => {
133 if (protocolResponse != null) {
134 this.sendResponse(protocolResponse)
135 }
136 })
137 .catch(Constants.EMPTY_FUNCTION)
138 })
139 } else {
140 throw new BaseError(`Unsupported HTTP method: '${req.method}'`)
141 }
142 } catch (error) {
143 logger.error(
144 `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`,
145 error
146 )
147 this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }))
148 }
149 }
150
151 private responseStatusToStatusCode (status: ResponseStatus): StatusCodes {
152 switch (status) {
153 case ResponseStatus.SUCCESS:
154 return StatusCodes.OK
155 case ResponseStatus.FAILURE:
156 return StatusCodes.BAD_REQUEST
157 default:
158 return StatusCodes.INTERNAL_SERVER_ERROR
159 }
160 }
161 }