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