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