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