UNPKG

@azure/web-pubsub-express

Version:
416 lines 18.3 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudEventsDispatcher = void 0; const tslib_1 = require("tslib"); const utils = tslib_1.__importStar(require("./utils.js")); const node_url_1 = require("node:url"); const logger_js_1 = require("./logger.js"); const mqttV311ConnectReturnCode_js_1 = require("./enum/MqttErrorCodes/mqttV311ConnectReturnCode.js"); const mqttV500ConnectReasonCode_js_1 = require("./enum/MqttErrorCodes/mqttV500ConnectReasonCode.js"); var EventType; (function (EventType) { EventType[EventType["Connect"] = 0] = "Connect"; EventType[EventType["Connected"] = 1] = "Connected"; EventType[EventType["Disconnected"] = 2] = "Disconnected"; EventType[EventType["UserEvent"] = 3] = "UserEvent"; })(EventType || (EventType = {})); function getConnectResponseHandler(connectRequest, response) { const states = connectRequest.context.states; let modified = false; const handler = { setState(name, value) { states[name] = value; modified = true; }, success(res) { if (modified) { response.setHeader("ce-connectionState", utils.toBase64JsonString(states)); } if (res === undefined) { response.statusCode = 204; response.end(); } else { response.statusCode = 200; response.setHeader("Content-Type", "application/json; charset=utf-8"); response.end(JSON.stringify(res)); } }, fail(code, detail) { handleConnectErrorResponse(connectRequest, response, code, detail); }, failWith(res) { if ("mqtt" in res) { response.statusCode = getStatusCodeFromMqttConnectCode(res.mqtt.code); response.setHeader("Content-Type", "application/json; charset=utf-8"); response.end(JSON.stringify(res)); } else { handleConnectErrorResponse(connectRequest, response, res.code, res.detail); } }, }; return handler; } function getUserEventResponseHandler(userRequest, response) { const states = userRequest.context.states; let modified = false; const handler = { setState(name, value) { modified = true; states[name] = value; }, success(data, dataType) { response.statusCode = 200; if (modified) { response.setHeader("ce-connectionState", utils.toBase64JsonString(states)); } switch (dataType) { case "json": response.setHeader("Content-Type", "application/json; charset=utf-8"); break; case "text": response.setHeader("Content-Type", "text/plain; charset=utf-8"); break; default: response.setHeader("Content-Type", "application/octet-stream"); break; } response.end(data !== null && data !== void 0 ? data : ""); }, fail(code, detail) { response.statusCode = code; response.end(detail !== null && detail !== void 0 ? detail : ""); }, }; return handler; } function getContext(request, origin) { const baseContext = { signature: utils.getHttpHeader(request, "ce-signature"), userId: utils.getHttpHeader(request, "ce-userid"), hub: utils.getHttpHeader(request, "ce-hub"), connectionId: utils.getHttpHeader(request, "ce-connectionid"), eventName: utils.getHttpHeader(request, "ce-eventname"), origin: origin, states: utils.fromBase64JsonString(utils.getHttpHeader(request, "ce-connectionstate")), clientProtocol: "default", }; if (isMqttRequest(request)) { const mqttProperties = { physicalConnectionId: utils.getHttpHeader(request, "ce-physicalConnectionId"), sessionId: utils.getHttpHeader(request, "ce-sessionId"), }; return Object.assign(Object.assign({}, baseContext), { clientProtocol: "mqtt", mqtt: mqttProperties }); } else { return baseContext; } } function tryGetWebPubSubEvent(req) { // check ce-type to see if it is a valid WebPubSub CloudEvent request const prefix = "azure.webpubsub."; const connect = "azure.webpubsub.sys.connect"; const connected = "azure.webpubsub.sys.connected"; const disconnectd = "azure.webpubsub.sys.disconnected"; const userPrefix = "azure.webpubsub.user."; const type = utils.getHttpHeader(req, "ce-type"); if (!(type === null || type === void 0 ? void 0 : type.startsWith(prefix))) { return undefined; } if (type.startsWith(userPrefix)) { return EventType.UserEvent; } switch (type) { case connect: return EventType.Connect; case connected: return EventType.Connected; case disconnectd: return EventType.Disconnected; default: return undefined; } } function getStatusCodeFromMqttConnectCode(mqttConnectCode) { if (mqttConnectCode < 0x80) { switch (mqttConnectCode) { case mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.UnacceptableProtocolVersion: case mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.IdentifierRejected: return 400; // BadRequest case mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.ServerUnavailable: return 503; // ServiceUnavailable case mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.BadUsernameOrPassword: case mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.NotAuthorized: return 401; // Unauthorized default: logger_js_1.logger.warning(`Invalid MQTT connect return code: ${mqttConnectCode}.`); return 500; // InternalServerError } } else { switch (mqttConnectCode) { case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.NotAuthorized: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.BadUserNameOrPassword: return 401; // Unauthorized case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.ClientIdentifierNotValid: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.MalformedPacket: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.UnsupportedProtocolVersion: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.BadAuthenticationMethod: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.TopicNameInvalid: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.PayloadFormatInvalid: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.ImplementationSpecificError: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.PacketTooLarge: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.RetainNotSupported: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.QosNotSupported: return 400; // BadRequest case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.QuotaExceeded: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.ConnectionRateExceeded: return 429; // TooManyRequests case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.Banned: return 403; // Forbidden case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.UseAnotherServer: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.ServerMoved: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.ServerUnavailable: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.ServerBusy: case mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.UnspecifiedError: return 500; // InternalServerError default: logger_js_1.logger.warning(`Invalid MQTT connect return code: ${mqttConnectCode}.`); return 500; // InternalServerError } } } function getMqttConnectCodeFromStatusCode(statusCode, protocolVersion) { if (protocolVersion === 4) { switch (statusCode) { case 400: return mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.BadUsernameOrPassword; case 401: return mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.NotAuthorized; case 500: return mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.ServerUnavailable; default: logger_js_1.logger.warning(`Unsupported HTTP Status Code: ${statusCode}.`); return mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.ServerUnavailable; } } else if (protocolVersion === 5) { switch (statusCode) { case 400: return mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.BadUserNameOrPassword; case 401: return mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.NotAuthorized; case 500: return mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.UnspecifiedError; default: logger_js_1.logger.warning(`Unsupported HTTP Status Code: ${statusCode}.`); return mqttV500ConnectReasonCode_js_1.MqttV500ConnectReasonCode.UnspecifiedError; } } else { logger_js_1.logger.warning(`Invalid MQTT protocol version: ${protocolVersion}.`); return mqttV311ConnectReturnCode_js_1.MqttV311ConnectReturnCode.UnacceptableProtocolVersion; } } function handleConnectErrorResponse(connectRequest, response, code, detail) { const isMqttReq = connectRequest.context.clientProtocol === "mqtt"; if (isMqttReq) { const protocolVersion = connectRequest.mqtt.protocolVersion; const mqttErrorResponse = { mqtt: { code: getMqttConnectCodeFromStatusCode(code, protocolVersion), reason: detail, }, }; response.statusCode = code; response.setHeader("Content-Type", "application/json; charset=utf-8"); response.end(JSON.stringify(mqttErrorResponse)); } else { response.statusCode = code; response.end(detail !== null && detail !== void 0 ? detail : ""); } } function isWebPubSubRequest(req) { return utils.getHttpHeader(req, "ce-awpsversion") !== undefined; } function isMqttRequest(req) { const subprotocol = utils.getHttpHeader(req, "ce-subprotocol"); const physicalConnectionId = utils.getHttpHeader(req, "ce-physicalConnectionId"); return (subprotocol !== undefined && subprotocol.toLowerCase().includes("mqtt") && physicalConnectionId !== undefined); } async function readUserEventRequest(request, origin) { const contentTypeheader = utils.getHttpHeader(request, "content-type"); if (contentTypeheader === undefined) { return undefined; } const contentType = contentTypeheader.split(";")[0].trim(); switch (contentType) { case "application/octet-stream": return { context: getContext(request, origin), data: await utils.readRequestBody(request), dataType: "binary", }; case "application/json": return { context: getContext(request, origin), data: JSON.parse((await utils.readRequestBody(request)).toString()), dataType: "json", }; case "text/plain": return { context: getContext(request, origin), data: (await utils.readRequestBody(request)).toString(), dataType: "text", }; default: return undefined; } } async function readSystemEventRequest(request, origin) { const body = (await utils.readRequestBody(request)).toString(); const parsedRequest = JSON.parse(body); parsedRequest.context = getContext(request, origin); return parsedRequest; } /** * @internal */ class CloudEventsDispatcher { constructor(hub, eventHandler) { this.hub = hub; this.eventHandler = eventHandler; this._allowAll = true; this._allowedOrigins = []; if (Array.isArray(eventHandler)) { throw new Error("Unexpected WebPubSubEventHandlerOptions"); } if ((eventHandler === null || eventHandler === void 0 ? void 0 : eventHandler.allowedEndpoints) !== undefined) { this._allowedOrigins = eventHandler.allowedEndpoints.map((endpoint) => new node_url_1.URL(endpoint).host.toLowerCase()); this._allowAll = false; } } handlePreflight(req, res) { if (!isWebPubSubRequest(req)) { return false; } const origin = utils.getHttpHeader(req, "webhook-request-origin"); if (origin === undefined) { logger_js_1.logger.warning("Expecting webhook-request-origin header."); res.statusCode = 400; } else if (this._allowAll) { res.setHeader("WebHook-Allowed-Origin", "*"); } else { // service to do the check res.setHeader("WebHook-Allowed-Origin", this._allowedOrigins); } res.end(); return true; } async handleRequest(request, response) { var _a, _b, _c, _d; if (!isWebPubSubRequest(request)) { return false; } // check if it is a valid WebPubSub cloud events const origin = utils.getHttpHeader(request, "webhook-request-origin"); if (origin === undefined) { return false; } const eventType = tryGetWebPubSubEvent(request); if (eventType === undefined) { return false; } // check if hub matches const hub = utils.getHttpHeader(request, "ce-hub"); if ((hub === null || hub === void 0 ? void 0 : hub.toUpperCase()) !== this.hub.toUpperCase()) { return false; } const isMqtt = isMqttRequest(request); // No need to read body if handler is not specified switch (eventType) { case EventType.Connect: if (!((_a = this.eventHandler) === null || _a === void 0 ? void 0 : _a.handleConnect)) { if (isMqtt) response.statusCode = 204; response.end(); return true; } break; case EventType.Connected: if (!((_b = this.eventHandler) === null || _b === void 0 ? void 0 : _b.onConnected)) { response.end(); return true; } break; case EventType.Disconnected: if (!((_c = this.eventHandler) === null || _c === void 0 ? void 0 : _c.onDisconnected)) { response.end(); return true; } break; case EventType.UserEvent: if (!((_d = this.eventHandler) === null || _d === void 0 ? void 0 : _d.handleUserEvent)) { response.end(); return true; } break; default: logger_js_1.logger.warning(`Unknown EventType ${eventType}`); return false; } switch (eventType) { case EventType.Connect: { const connectRequest = isMqtt ? await readSystemEventRequest(request, origin) : await readSystemEventRequest(request, origin); // service passes out query property, assign it to queries connectRequest.queries = connectRequest.query; logger_js_1.logger.verbose(connectRequest); this.eventHandler.handleConnect(connectRequest, getConnectResponseHandler(connectRequest, response)); return true; } case EventType.Connected: { // for unblocking events, we responds to the service as early as possible response.end(); const connectedRequest = await readSystemEventRequest(request, origin); logger_js_1.logger.verbose(connectedRequest); this.eventHandler.onConnected(connectedRequest); return true; } case EventType.Disconnected: { // for unblocking events, we responds to the service as early as possible response.end(); const disconnectedRequest = isMqtt ? await readSystemEventRequest(request, origin) : await readSystemEventRequest(request, origin); logger_js_1.logger.verbose(disconnectedRequest); this.eventHandler.onDisconnected(disconnectedRequest); return true; } case EventType.UserEvent: { const userRequest = await readUserEventRequest(request, origin); if (userRequest === undefined) { logger_js_1.logger.warning(`Unsupported content type ${utils.getHttpHeader(request, "content-type")}`); return false; } logger_js_1.logger.verbose(userRequest); this.eventHandler.handleUserEvent(userRequest, getUserEventResponseHandler(userRequest, response)); return true; } default: logger_js_1.logger.warning(`Unknown EventType ${eventType}`); return false; } } } exports.CloudEventsDispatcher = CloudEventsDispatcher; //# sourceMappingURL=cloudEventsDispatcher.js.map