// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
export const convertDateToISOString = <T extends JsonType>(object: T): void => {
- // eslint-disable-next-line @typescript-eslint/no-for-in-array
- for (const key in object) {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
- if (isDate(object![key])) {
- ;(object[key] as unknown as string) = (object[key] as Date).toISOString()
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
- } else if (typeof object![key] === 'object' && object[key] !== null) {
- convertDateToISOString<T>(object[key] as T)
+ for (const [key, value] of Object.entries(object as Record<string, unknown>)) {
+ if (isDate(value)) {
+ try {
+ ;(object as Record<string, unknown>)[key] = value.toISOString()
+ } catch {
+ // Ignore date conversion error
+ }
+ } else if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const item = value[i]
+ if (isDate(item)) {
+ try {
+ value[i] = item.toISOString() as unknown as typeof item
+ } catch {
+ // Ignore date conversion error
+ }
+ } else if (typeof item === 'object' && item !== null) {
+ convertDateToISOString(item as T)
+ }
+ }
+ } else if (typeof value === 'object' && value !== null) {
+ convertDateToISOString<T>(value as T)
}
}
}
Number.parseInt(socSampledValueTemplate.value),
socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
)
- : randomInt(socMinimumValue, socMaximumValue)
+ : randomInt(socMinimumValue, socMaximumValue + 1)
meterValue.sampledValue.push(
buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
)
}: phase ${
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
- }, connector id ${connectorId.toString()}, transaction id $
- connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ }, connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${
meterValue.sampledValue[sampledValuesPerPhaseIndex].value
}/${connectorMaximumAmperage.toString()}`
)
},
...options,
}
- const parsedValue = Number.parseInt(value ?? '')
+ const parsedValue = Number.parseFloat(value ?? '')
if (options.limitationEnabled) {
return max(
min(
type UIServerConfiguration,
} from '../../types/index.js'
import {
- Constants,
generateUUID,
isNotEmptyString,
JSONStringify,
.end(`${StatusCodes.UNAUTHORIZED.toString()} Unauthorized`)
res.destroy()
req.destroy()
+ return
}
- })
- // Expected request URL pathname: /ui/:version/:procedureName
- const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [
- Protocol,
- ProtocolVersion,
- ProcedureName
- ]
- const uuid = generateUUID()
- this.responseHandlers.set(uuid, res)
- try {
- const fullProtocol = `${protocol}${version}`
- if (!isProtocolAndVersionSupported(fullProtocol)) {
- throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`)
- }
- this.registerProtocolVersionUIService(version)
- req.on('error', error => {
- logger.error(
- `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`,
- error
- )
+
+ const uuid = generateUUID()
+ this.responseHandlers.set(uuid, res)
+ res.on('close', () => {
+ this.responseHandlers.delete(uuid)
})
- if (req.method === HttpMethods.POST) {
+ try {
+ // Expected request URL pathname: /ui/:version/:procedureName
+ const rawUrl = req.url ?? ''
+ const { pathname } = new URL(rawUrl, 'http://localhost')
+ const parts = pathname.split('/').filter(Boolean)
+ if (parts.length < 3) {
+ throw new BaseError(
+ `Malformed URL path: '${pathname}' (expected /ui/:version/:procedureName)`
+ )
+ }
+ const [protocol, version, procedureName] = parts as [
+ Protocol,
+ ProtocolVersion,
+ ProcedureName
+ ]
+ const fullProtocol = `${protocol}${version}`
+ if (!isProtocolAndVersionSupported(fullProtocol)) {
+ throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`)
+ }
+ this.registerProtocolVersionUIService(version)
+
+ req.on('error', error => {
+ logger.error(
+ `${this.logPrefix(moduleName, 'requestListener.req.onerror')} Error on HTTP request:`,
+ error
+ )
+ if (!res.headersSent) {
+ this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }))
+ } else {
+ this.responseHandlers.delete(uuid)
+ }
+ })
+
+ if (req.method !== HttpMethods.POST) {
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ throw new BaseError(`Unsupported HTTP method: '${req.method}'`)
+ }
+
const bodyBuffer: Uint8Array[] = []
req
.on('data', (chunk: Uint8Array) => {
)
return
}
- this.uiServices
- .get(version)
- ?.requestHandler(this.buildProtocolRequest(uuid, procedureName, requestPayload))
+ const service = this.uiServices.get(version)
+ if (service == null || typeof service.requestHandler !== 'function') {
+ this.sendResponse(
+ this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE })
+ )
+ return
+ }
+ // eslint-disable-next-line promise/no-promise-in-callback
+ service
+ .requestHandler(this.buildProtocolRequest(uuid, procedureName, requestPayload))
.then((protocolResponse?: ProtocolResponse) => {
if (protocolResponse != null) {
this.sendResponse(protocolResponse)
+ } else {
+ this.sendResponse(
+ this.buildProtocolResponse(uuid, { status: ResponseStatus.SUCCESS })
+ )
}
return undefined
})
- .catch(Constants.EMPTY_FUNCTION)
+ .catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix(moduleName, 'requestListener.handler.catch')} UI service handler error:`,
+ error
+ )
+ this.sendResponse(
+ this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE })
+ )
+ })
})
- } else {
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- throw new BaseError(`Unsupported HTTP method: '${req.method}'`)
+ } catch (error) {
+ logger.error(
+ `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`,
+ error
+ )
+ this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }))
}
- } catch (error) {
- logger.error(
- `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`,
- error
- )
- this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }))
- }
+ })
}
private responseStatusToStatusCode (status: ResponseStatus): StatusCodes {
public start (): void {
this.webSocketServer.on('connection', (ws: WebSocket, _req: IncomingMessage): void => {
- if (!isProtocolAndVersionSupported(ws.protocol)) {
+ const protocol = ws.protocol
+ const protocolAndVersion = getProtocolAndVersion(protocol)
+ if (protocolAndVersion == null || !isProtocolAndVersionSupported(protocol)) {
logger.error(
`${this.logPrefix(
moduleName,
'start.server.onconnection'
- )} Unsupported UI protocol version: '${ws.protocol}'`
+ )} Unsupported UI protocol version: '${protocol}'`
)
ws.close(WebSocketCloseEventStatusCode.CLOSE_PROTOCOL_ERROR)
+ return
}
- const [, version] = getProtocolAndVersion(ws.protocol)
+ const [, version] = protocolAndVersion
this.registerProtocolVersionUIService(version)
ws.on('message', rawData => {
const request = this.validateRawDataRequest(rawData)
return undefined
})
.catch(Constants.EMPTY_FUNCTION)
+ .finally(() => {
+ this.responseHandlers.delete(requestId)
+ })
})
ws.on('error', error => {
logger.error(`${this.logPrefix(moduleName, 'start.ws.onerror')} WebSocket error:`, error)
code
)}' - '${reason.toString()}'`
)
+ for (const [responseId, responseHandlerWs] of this.responseHandlers) {
+ if (responseHandlerWs === ws) this.responseHandlers.delete(responseId)
+ }
})
})
this.httpServer.on('connect', (req: IncomingMessage, socket: Duplex, _head: Buffer) => {
- if (req.headers.connection !== 'Upgrade' || req.headers.upgrade !== 'websocket') {
+ const connectionHeader = req.headers.connection ?? ''
+ const upgradeHeader = req.headers.upgrade ?? ''
+ if (!/upgrade/i.test(connectionHeader) || !/^websocket$/i.test(upgradeHeader)) {
socket.write(`HTTP/1.1 ${StatusCodes.BAD_REQUEST.toString()} Bad Request\r\n\r\n`)
socket.destroy()
}
}
socket.on('error', onSocketError)
this.authenticate(req, err => {
+ socket.removeListener('error', onSocketError)
if (err != null) {
socket.write(`HTTP/1.1 ${StatusCodes.UNAUTHORIZED.toString()} Unauthorized\r\n\r\n`)
socket.destroy()
)
}
})
- socket.removeListener('error', onSocketError)
})
this.startHttpServer()
}
return false
}
+ if (typeof request[1] !== 'string') {
+ logger.error(
+ `${this.logPrefix(
+ moduleName,
+ 'validateRawDataRequest'
+ )} UI protocol request procedure field must be a string:`,
+ request
+ )
+ return false
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!(typeof request[2] === 'object' && request[2] !== null)) {
+ logger.error(
+ `${this.logPrefix(
+ moduleName,
+ 'validateRawDataRequest'
+ )} UI protocol request payload field must be an object or an array:`,
+ request
+ )
+ return false
+ }
+
return request
}
}
const type = (value: unknown): string => {
if (value === null) return 'Null'
if (value === undefined) return 'Undefined'
- if (typeof value === 'string') return 'String'
if (Number.isNaN(value)) return 'NaN'
if (Array.isArray(value)) return 'Array'
return Object.prototype.toString.call(value).slice(8, -1)
export const isEmpty = (value: unknown): boolean => {
const valueType = type(value)
- if (['NaN', 'Null', 'Number', 'Undefined'].includes(valueType)) {
+ if (['BigInt', 'Boolean', 'NaN', 'Null', 'Number', 'Undefined'].includes(valueType)) {
return false
}
if (!value) return true