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; | |
45 | res.writeHead(this.responseStatusToStatusCode(payload.status), { | |
46 | 'Content-Type': 'application/json', | |
47 | }); | |
48 | res.end(JSON.stringify(payload)); | |
49 | this.responseHandlers.delete(uuid); | |
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 JB |
59 | ); |
60 | } | |
61 | } | |
62 | ||
0d2cec76 JB |
63 | public logPrefix(modName?: string, methodName?: string, prefixSuffix?: string): string { |
64 | const logMsgPrefix = prefixSuffix ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server'; | |
1f7fa4de | 65 | const logMsg = |
0d2cec76 | 66 | modName && methodName ? ` ${logMsgPrefix} | ${modName}.${methodName}:` : ` ${logMsgPrefix} |`; |
1f7fa4de JB |
67 | return Utils.logPrefix(logMsg); |
68 | } | |
69 | ||
70 | private requestListener(req: IncomingMessage, res: ServerResponse): void { | |
623b39b5 JB |
71 | this.authenticate(req, (err) => { |
72 | if (err) { | |
73 | res.setHeader('Content-Type', 'text/plain'); | |
74 | res.setHeader('WWW-Authenticate', 'Basic realm=users'); | |
75 | res.writeHead(StatusCodes.UNAUTHORIZED); | |
76 | res.end(`${StatusCodes.UNAUTHORIZED} Unauthorized`); | |
77 | req.destroy(); | |
78 | res.destroy(); | |
79 | } | |
80 | }); | |
1f7fa4de JB |
81 | // Expected request URL pathname: /ui/:version/:procedureName |
82 | const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [ | |
83 | Protocol, | |
84 | ProtocolVersion, | |
85 | ProcedureName | |
86 | ]; | |
87 | const uuid = Utils.generateUUID(); | |
94dc3080 | 88 | this.responseHandlers.set(uuid, res); |
1f7fa4de | 89 | try { |
a92929f1 | 90 | if (UIServiceUtils.isProtocolAndVersionSupported(protocol, version) === false) { |
1f7fa4de JB |
91 | throw new BaseError(`Unsupported UI protocol version: '/${protocol}/${version}'`); |
92 | } | |
daa6505e | 93 | this.registerProtocolVersionUIService(version); |
1f7fa4de JB |
94 | req.on('error', (error) => { |
95 | logger.error( | |
a745e412 | 96 | `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`, |
1f7fa4de JB |
97 | error |
98 | ); | |
99 | }); | |
1f7fa4de JB |
100 | if (req.method === 'POST') { |
101 | const bodyBuffer = []; | |
1f7fa4de JB |
102 | req |
103 | .on('data', (chunk) => { | |
104 | bodyBuffer.push(chunk); | |
105 | }) | |
106 | .on('end', () => { | |
a745e412 | 107 | const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload; |
1f7fa4de JB |
108 | this.uiServices |
109 | .get(version) | |
852a4c5f | 110 | .requestHandler(this.buildProtocolRequest(uuid, procedureName, body ?? {})) |
1f7fa4de | 111 | .catch(() => { |
976d11ec | 112 | /* Error caught by AbstractUIService */ |
1f7fa4de JB |
113 | }); |
114 | }); | |
115 | } else { | |
116 | throw new BaseError(`Unsupported HTTP method: '${req.method}'`); | |
117 | } | |
118 | } catch (error) { | |
a745e412 JB |
119 | logger.error( |
120 | `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`, | |
121 | error | |
122 | ); | |
852a4c5f | 123 | this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE })); |
1f7fa4de JB |
124 | } |
125 | } | |
126 | ||
771633ff JB |
127 | private responseStatusToStatusCode(status: ResponseStatus): StatusCodes { |
128 | switch (status) { | |
129 | case ResponseStatus.SUCCESS: | |
130 | return StatusCodes.OK; | |
131 | case ResponseStatus.FAILURE: | |
132 | return StatusCodes.BAD_REQUEST; | |
133 | default: | |
134 | return StatusCodes.INTERNAL_SERVER_ERROR; | |
135 | } | |
136 | } | |
1f7fa4de | 137 | } |