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'; | |
ed3d2808 | 19 | import { UIServerUtils } from './UIServerUtils'; |
1f7fa4de JB |
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); |
a307349b | 30 | this.startHttpServer(); |
1f7fa4de JB |
31 | } |
32 | ||
1f7fa4de | 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars |
5e3cb728 | 34 | public sendRequest(request: ProtocolRequest): void { |
1f7fa4de JB |
35 | // This is intentionally left blank |
36 | } | |
37 | ||
5e3cb728 JB |
38 | public sendResponse(response: ProtocolResponse): void { |
39 | const [uuid, payload] = response; | |
976d11ec JB |
40 | try { |
41 | if (this.responseHandlers.has(uuid) === true) { | |
42 | const res = this.responseHandlers.get(uuid) as ServerResponse; | |
b2e2c274 JB |
43 | res |
44 | .writeHead(this.responseStatusToStatusCode(payload.status), { | |
45 | 'Content-Type': 'application/json', | |
46 | }) | |
47 | .end(JSON.stringify(payload)); | |
976d11ec JB |
48 | } else { |
49 | logger.error( | |
50 | `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}` | |
51 | ); | |
52 | } | |
53 | } catch (error) { | |
1f7fa4de | 54 | logger.error( |
976d11ec JB |
55 | `${this.logPrefix(moduleName, 'sendResponse')} Error at sending response id '${uuid}':`, |
56 | error | |
1f7fa4de | 57 | ); |
e2c77f10 JB |
58 | } finally { |
59 | this.responseHandlers.delete(uuid); | |
1f7fa4de JB |
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) { | |
b2e2c274 JB |
73 | res |
74 | .writeHead(StatusCodes.UNAUTHORIZED, { | |
75 | 'Content-Type': 'text/plain', | |
76 | 'WWW-Authenticate': 'Basic realm=users', | |
77 | }) | |
78 | .end(`${StatusCodes.UNAUTHORIZED} Unauthorized`) | |
79 | .destroy(); | |
623b39b5 | 80 | req.destroy(); |
623b39b5 JB |
81 | } |
82 | }); | |
1f7fa4de JB |
83 | // Expected request URL pathname: /ui/:version/:procedureName |
84 | const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [ | |
85 | Protocol, | |
86 | ProtocolVersion, | |
87 | ProcedureName | |
88 | ]; | |
89 | const uuid = Utils.generateUUID(); | |
94dc3080 | 90 | this.responseHandlers.set(uuid, res); |
1f7fa4de | 91 | try { |
7cb5b17f | 92 | const fullProtocol = `${protocol}${version}`; |
ed3d2808 | 93 | if (UIServerUtils.isProtocolAndVersionSupported(fullProtocol) === false) { |
7cb5b17f | 94 | throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`); |
1f7fa4de | 95 | } |
daa6505e | 96 | this.registerProtocolVersionUIService(version); |
1f7fa4de JB |
97 | req.on('error', (error) => { |
98 | logger.error( | |
a745e412 | 99 | `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`, |
1f7fa4de JB |
100 | error |
101 | ); | |
102 | }); | |
1f7fa4de JB |
103 | if (req.method === 'POST') { |
104 | const bodyBuffer = []; | |
1f7fa4de JB |
105 | req |
106 | .on('data', (chunk) => { | |
107 | bodyBuffer.push(chunk); | |
108 | }) | |
109 | .on('end', () => { | |
a745e412 | 110 | const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload; |
1f7fa4de JB |
111 | this.uiServices |
112 | .get(version) | |
852a4c5f | 113 | .requestHandler(this.buildProtocolRequest(uuid, procedureName, body ?? {})) |
1f7fa4de | 114 | .catch(() => { |
976d11ec | 115 | /* Error caught by AbstractUIService */ |
1f7fa4de JB |
116 | }); |
117 | }); | |
118 | } else { | |
119 | throw new BaseError(`Unsupported HTTP method: '${req.method}'`); | |
120 | } | |
121 | } catch (error) { | |
a745e412 JB |
122 | logger.error( |
123 | `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`, | |
124 | error | |
125 | ); | |
852a4c5f | 126 | this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE })); |
1f7fa4de JB |
127 | } |
128 | } | |
129 | ||
771633ff JB |
130 | private responseStatusToStatusCode(status: ResponseStatus): StatusCodes { |
131 | switch (status) { | |
132 | case ResponseStatus.SUCCESS: | |
133 | return StatusCodes.OK; | |
134 | case ResponseStatus.FAILURE: | |
135 | return StatusCodes.BAD_REQUEST; | |
136 | default: | |
137 | return StatusCodes.INTERNAL_SERVER_ERROR; | |
138 | } | |
139 | } | |
1f7fa4de | 140 | } |