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