Commit | Line | Data |
---|---|---|
66a7748d | 1 | import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http' |
1f7fa4de | 2 | |
66a7748d | 3 | import { StatusCodes } from 'http-status-codes' |
1f7fa4de | 4 | |
66a7748d JB |
5 | import { AbstractUIServer } from './AbstractUIServer.js' |
6 | import { UIServerUtils } from './UIServerUtils.js' | |
7 | import { BaseError } from '../../exception/index.js' | |
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, |
66a7748d JB |
17 | type UIServerConfiguration |
18 | } from '../../types/index.js' | |
19 | import { Constants, generateUUID, isNotEmptyString, logPrefix, logger } from '../../utils/index.js' | |
1f7fa4de | 20 | |
66a7748d | 21 | const moduleName = 'UIHttpServer' |
1f7fa4de | 22 | |
1185579a JB |
23 | enum HttpMethods { |
24 | GET = 'GET', | |
25 | PUT = 'PUT', | |
26 | POST = 'POST', | |
a807045b | 27 | PATCH = 'PATCH', |
1185579a JB |
28 | } |
29 | ||
268a74bb | 30 | export 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) | |
5199f9fd | 125 | ?.requestHandler(this.buildProtocolRequest(uuid, procedureName, body)) |
4a3807d1 | 126 | .then((protocolResponse?: ProtocolResponse) => { |
66a7748d JB |
127 | if (protocolResponse != null) { |
128 | this.sendResponse(protocolResponse) | |
0b22144c JB |
129 | } |
130 | }) | |
66a7748d JB |
131 | .catch(Constants.EMPTY_FUNCTION) |
132 | }) | |
1f7fa4de | 133 | } else { |
66a7748d | 134 | throw new BaseError(`Unsupported HTTP method: '${req.method}'`) |
1f7fa4de JB |
135 | } |
136 | } catch (error) { | |
a745e412 JB |
137 | logger.error( |
138 | `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`, | |
66a7748d JB |
139 | error |
140 | ) | |
141 | this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE })) | |
1f7fa4de JB |
142 | } |
143 | } | |
144 | ||
66a7748d | 145 | private responseStatusToStatusCode (status: ResponseStatus): StatusCodes { |
771633ff JB |
146 | switch (status) { |
147 | case ResponseStatus.SUCCESS: | |
66a7748d | 148 | return StatusCodes.OK |
771633ff | 149 | case ResponseStatus.FAILURE: |
66a7748d | 150 | return StatusCodes.BAD_REQUEST |
771633ff | 151 | default: |
66a7748d | 152 | return StatusCodes.INTERNAL_SERVER_ERROR |
771633ff JB |
153 | } |
154 | } | |
1f7fa4de | 155 | } |