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