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