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 | ||
23 | type responseHandler = { procedureName: ProcedureName; res: ServerResponse }; | |
24 | ||
25 | export default class UIHttpServer extends AbstractUIServer { | |
26 | private readonly responseHandlers: Map<string, responseHandler>; | |
27 | ||
eb3abc4f JB |
28 | public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) { |
29 | super(uiServerConfiguration); | |
1f7fa4de JB |
30 | this.responseHandlers = new Map<string, responseHandler>(); |
31 | } | |
32 | ||
33 | public start(): void { | |
daa6505e JB |
34 | this.httpServer.on('connection', (socket) => { |
35 | this.sockets.add(socket); | |
36 | socket.on('close', () => { | |
37 | this.sockets.delete(socket); | |
38 | }); | |
39 | }); | |
40 | this.httpServer.on('request', this.requestListener.bind(this) as RequestListener); | |
eb3abc4f JB |
41 | if (this.httpServer.listening === false) { |
42 | this.httpServer.listen(this.uiServerConfiguration.options); | |
10d244c0 | 43 | } |
1f7fa4de JB |
44 | } |
45 | ||
1f7fa4de | 46 | // eslint-disable-next-line @typescript-eslint/no-unused-vars |
5e3cb728 | 47 | public sendRequest(request: ProtocolRequest): void { |
1f7fa4de JB |
48 | // This is intentionally left blank |
49 | } | |
50 | ||
5e3cb728 JB |
51 | public sendResponse(response: ProtocolResponse): void { |
52 | const [uuid, payload] = response; | |
10d244c0 | 53 | if (this.responseHandlers.has(uuid) === true) { |
49bc4b80 | 54 | const { res } = this.responseHandlers.get(uuid); |
d0b0b492 JB |
55 | res.writeHead(this.responseStatusToStatusCode(payload.status), { |
56 | 'Content-Type': 'application/json', | |
57 | }); | |
daa6505e | 58 | res.end(JSON.stringify(payload)); |
1f7fa4de JB |
59 | this.responseHandlers.delete(uuid); |
60 | } else { | |
61 | logger.error( | |
5e3cb728 | 62 | `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}` |
1f7fa4de JB |
63 | ); |
64 | } | |
65 | } | |
66 | ||
0d2cec76 JB |
67 | public logPrefix(modName?: string, methodName?: string, prefixSuffix?: string): string { |
68 | const logMsgPrefix = prefixSuffix ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server'; | |
1f7fa4de | 69 | const logMsg = |
0d2cec76 | 70 | modName && methodName ? ` ${logMsgPrefix} | ${modName}.${methodName}:` : ` ${logMsgPrefix} |`; |
1f7fa4de JB |
71 | return Utils.logPrefix(logMsg); |
72 | } | |
73 | ||
74 | private requestListener(req: IncomingMessage, res: ServerResponse): void { | |
687de086 | 75 | if (this.authenticate(req) === false) { |
eb3abc4f JB |
76 | res.setHeader('Content-Type', 'text/plain'); |
77 | res.setHeader('WWW-Authenticate', 'Basic realm=users'); | |
78 | res.writeHead(StatusCodes.UNAUTHORIZED); | |
79 | res.end(`${StatusCodes.UNAUTHORIZED} Unauthorized`); | |
80 | return; | |
81 | } | |
1f7fa4de JB |
82 | // Expected request URL pathname: /ui/:version/:procedureName |
83 | const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [ | |
84 | Protocol, | |
85 | ProtocolVersion, | |
86 | ProcedureName | |
87 | ]; | |
88 | const uuid = Utils.generateUUID(); | |
89 | this.responseHandlers.set(uuid, { procedureName, res }); | |
90 | try { | |
a92929f1 | 91 | if (UIServiceUtils.isProtocolAndVersionSupported(protocol, version) === false) { |
1f7fa4de JB |
92 | throw new BaseError(`Unsupported UI protocol version: '/${protocol}/${version}'`); |
93 | } | |
daa6505e | 94 | this.registerProtocolVersionUIService(version); |
1f7fa4de JB |
95 | req.on('error', (error) => { |
96 | logger.error( | |
a745e412 | 97 | `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`, |
1f7fa4de JB |
98 | error |
99 | ); | |
100 | }); | |
1f7fa4de JB |
101 | if (req.method === 'POST') { |
102 | const bodyBuffer = []; | |
1f7fa4de JB |
103 | req |
104 | .on('data', (chunk) => { | |
105 | bodyBuffer.push(chunk); | |
106 | }) | |
107 | .on('end', () => { | |
a745e412 | 108 | const body = JSON.parse(Buffer.concat(bodyBuffer).toString()) as RequestPayload; |
1f7fa4de JB |
109 | this.uiServices |
110 | .get(version) | |
852a4c5f | 111 | .requestHandler(this.buildProtocolRequest(uuid, procedureName, body ?? {})) |
1f7fa4de | 112 | .catch(() => { |
852a4c5f JB |
113 | this.sendResponse( |
114 | this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }) | |
115 | ); | |
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 | ||
687de086 JB |
130 | private authenticate(req: IncomingMessage): boolean { |
131 | if (this.isBasicAuthEnabled() === true) { | |
132 | if (this.isValidBasicAuth(req) === true) { | |
133 | return true; | |
134 | } | |
135 | return false; | |
136 | } | |
137 | return true; | |
138 | } | |
139 | ||
771633ff JB |
140 | private responseStatusToStatusCode(status: ResponseStatus): StatusCodes { |
141 | switch (status) { | |
142 | case ResponseStatus.SUCCESS: | |
143 | return StatusCodes.OK; | |
144 | case ResponseStatus.FAILURE: | |
145 | return StatusCodes.BAD_REQUEST; | |
146 | default: | |
147 | return StatusCodes.INTERNAL_SERVER_ERROR; | |
148 | } | |
149 | } | |
1f7fa4de | 150 | } |