@azure/web-pubsub-express 1.0.0-beta.1 → 1.0.1-alpha.20211215.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,270 +2,338 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var tslib = require('tslib');
6
5
  var cloudevents = require('cloudevents');
7
6
  var url = require('url');
7
+ var logger$1 = require('@azure/logger');
8
8
 
9
9
  // Copyright (c) Microsoft Corporation.
10
10
  /**
11
+ * The \@azure/logger configuration for this package.
12
+ *
11
13
  * @internal
12
14
  */
13
- class CloudEventsDispatcher {
14
- constructor(hub, allowedEndpoints, eventHandler) {
15
- var _a;
16
- this.hub = hub;
17
- this.eventHandler = eventHandler;
18
- this._dumpRequest = (_a = eventHandler === null || eventHandler === void 0 ? void 0 : eventHandler.dumpRequest) !== null && _a !== void 0 ? _a : false;
19
- this._allowedOrigins = allowedEndpoints.map((endpoint) => endpoint === "*" ? "*" : new url.URL(endpoint).host);
15
+ const logger = logger$1.createClientLogger("web-pubsub-express");
16
+
17
+ // Copyright (c) Microsoft Corporation.
18
+ // Licensed under the MIT license.
19
+ function isJsonObject(obj) {
20
+ return obj && typeof obj === "object" && !Array.isArray(obj);
21
+ }
22
+ function toBase64JsonString(obj) {
23
+ return Buffer.from(JSON.stringify(obj)).toString("base64");
24
+ }
25
+ function fromBase64JsonString(base64String) {
26
+ if (base64String === undefined) {
27
+ return {};
20
28
  }
21
- processValidateRequest(req, res) {
22
- if (req.headers["webhook-request-origin"]) {
23
- res.setHeader("WebHook-Allowed-Origin", this._allowedOrigins);
24
- res.end();
25
- return true;
26
- }
27
- else {
28
- return false;
29
+ try {
30
+ const buf = Buffer.from(base64String, "base64").toString();
31
+ const parsed = JSON.parse(buf);
32
+ return isJsonObject(parsed) ? parsed : {};
33
+ }
34
+ catch (e) {
35
+ console.warn("Unexpected state format:" + e);
36
+ return {};
37
+ }
38
+ }
39
+ function getHttpHeader(req, key) {
40
+ const value = req.headers[key];
41
+ if (value === undefined) {
42
+ return undefined;
43
+ }
44
+ if (typeof value === "string") {
45
+ return value;
46
+ }
47
+ return value[0];
48
+ }
49
+ async function convertHttpToEvent(request) {
50
+ const normalized = {
51
+ headers: {},
52
+ body: ""
53
+ };
54
+ if (request.headers) {
55
+ for (const key in request.headers) {
56
+ if (Object.prototype.hasOwnProperty.call(request.headers, key)) {
57
+ const element = request.headers[key];
58
+ if (element !== undefined) {
59
+ normalized.headers[key.toLowerCase()] = element;
60
+ }
61
+ }
29
62
  }
30
63
  }
31
- processRequest(request, response) {
32
- var _a, _b, _c, _d, _e;
33
- return tslib.__awaiter(this, void 0, void 0, function* () {
34
- // check if it is a valid WebPubSub cloud events
35
- const origin = this.getSingleHeader(request, "webhook-request-origin");
36
- if (origin === undefined) {
37
- return false;
64
+ normalized.body = await readRequestBody(request);
65
+ return normalized;
66
+ }
67
+ function readRequestBody(req) {
68
+ return new Promise(function (resolve, reject) {
69
+ const chunks = [];
70
+ req.on("data", function (chunk) {
71
+ chunks.push(chunk);
72
+ });
73
+ req.on("end", function () {
74
+ const buffer = Buffer.concat(chunks);
75
+ resolve(buffer.toString());
76
+ });
77
+ // reject on request error
78
+ req.on("error", function (err) {
79
+ // This is not a "Second reject", just a different sort of failure
80
+ reject(err);
81
+ });
82
+ });
83
+ }
84
+
85
+ // Copyright (c) Microsoft Corporation.
86
+ var EventType;
87
+ (function (EventType) {
88
+ EventType[EventType["Connect"] = 0] = "Connect";
89
+ EventType[EventType["Connected"] = 1] = "Connected";
90
+ EventType[EventType["Disconnected"] = 2] = "Disconnected";
91
+ EventType[EventType["UserEvent"] = 3] = "UserEvent";
92
+ })(EventType || (EventType = {}));
93
+ function getConnectResponseHandler(connectRequest, response) {
94
+ const states = connectRequest.context.states;
95
+ let modified = false;
96
+ const handler = {
97
+ setState(name, value) {
98
+ states[name] = value;
99
+ modified = true;
100
+ },
101
+ success(res) {
102
+ response.statusCode = 200;
103
+ if (modified) {
104
+ response.setHeader("ce-connectionState", toBase64JsonString(states));
38
105
  }
39
- const eventType = this.tryGetWebPubSubEvent(request);
40
- if (eventType === undefined) {
41
- return false;
106
+ if (res === undefined) {
107
+ response.end();
42
108
  }
43
- // check if hub matches
44
- if (request.headers["ce-hub"] !== this.hub) {
45
- return false;
109
+ else {
110
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
111
+ response.end(JSON.stringify(res));
46
112
  }
47
- // No need to read body if handler is not specified
48
- switch (eventType) {
49
- case EventType.Connect:
50
- if (!((_a = this.eventHandler) === null || _a === void 0 ? void 0 : _a.handleConnect)) {
51
- response.statusCode = 401;
52
- response.end("Connect event handler is not configured.");
53
- return true;
54
- }
55
- break;
56
- case EventType.Connected:
57
- if (!((_b = this.eventHandler) === null || _b === void 0 ? void 0 : _b.onConnected)) {
58
- response.end();
59
- return true;
60
- }
61
- break;
62
- case EventType.Disconnected:
63
- if (!((_c = this.eventHandler) === null || _c === void 0 ? void 0 : _c.onDisconnected)) {
64
- response.end();
65
- return true;
66
- }
113
+ },
114
+ fail(code, detail) {
115
+ response.statusCode = code;
116
+ response.end(detail !== null && detail !== void 0 ? detail : "");
117
+ }
118
+ };
119
+ return handler;
120
+ }
121
+ function getUserEventResponseHandler(userRequest, response) {
122
+ const states = userRequest.context.states;
123
+ let modified = false;
124
+ const handler = {
125
+ setState(name, value) {
126
+ modified = true;
127
+ states[name] = value;
128
+ },
129
+ success(data, dataType) {
130
+ response.statusCode = 200;
131
+ if (modified) {
132
+ response.setHeader("ce-connectionState", toBase64JsonString(states));
133
+ }
134
+ switch (dataType) {
135
+ case "json":
136
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
67
137
  break;
68
- case EventType.UserEvent:
69
- if (!((_d = this.eventHandler) === null || _d === void 0 ? void 0 : _d.handleUserEvent)) {
70
- response.end();
71
- return true;
72
- }
138
+ case "text":
139
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
73
140
  break;
74
141
  default:
75
- console.warn(`Unknown EventType ${eventType}`);
76
- return false;
77
- }
78
- const eventRequest = yield this.convertHttpToEvent(request);
79
- const receivedEvent = cloudevents.HTTP.toEvent(eventRequest);
80
- if (this._dumpRequest) {
81
- console.log(receivedEvent);
142
+ response.setHeader("Content-Type", "application/octet-stream");
143
+ break;
82
144
  }
83
- switch (eventType) {
84
- case EventType.Connect: {
85
- const connectRequest = receivedEvent.data;
86
- connectRequest.context = this.GetContext(receivedEvent, origin);
87
- this.eventHandler.handleConnect(connectRequest, {
88
- success(res) {
89
- response.statusCode = 200;
90
- if (res === undefined) {
91
- response.end();
92
- }
93
- else {
94
- response.setHeader("Content-Type", "application/json; charset=utf-8");
95
- response.end(JSON.stringify(res));
96
- }
97
- },
98
- fail(code, detail) {
99
- response.statusCode = code;
100
- response.end(detail !== null && detail !== void 0 ? detail : "");
101
- }
102
- });
145
+ response.end(data !== null && data !== void 0 ? data : "");
146
+ },
147
+ fail(code, detail) {
148
+ response.statusCode = code;
149
+ response.end(detail !== null && detail !== void 0 ? detail : "");
150
+ }
151
+ };
152
+ return handler;
153
+ }
154
+ function getContext(ce, origin) {
155
+ const context = {
156
+ signature: ce["signature"],
157
+ userId: ce["userid"],
158
+ hub: ce["hub"],
159
+ connectionId: ce["connectionid"],
160
+ eventName: ce["eventname"],
161
+ origin: origin,
162
+ states: fromBase64JsonString(ce["connectionstate"])
163
+ };
164
+ // TODO: validation
165
+ return context;
166
+ }
167
+ function tryGetWebPubSubEvent(req) {
168
+ // check ce-type to see if it is a valid WebPubSub CloudEvent request
169
+ const prefix = "azure.webpubsub.";
170
+ const connect = "azure.webpubsub.sys.connect";
171
+ const connected = "azure.webpubsub.sys.connected";
172
+ const disconnectd = "azure.webpubsub.sys.disconnected";
173
+ const userPrefix = "azure.webpubsub.user.";
174
+ const type = getHttpHeader(req, "ce-type");
175
+ if (!(type === null || type === void 0 ? void 0 : type.startsWith(prefix))) {
176
+ return undefined;
177
+ }
178
+ if (type.startsWith(userPrefix)) {
179
+ return EventType.UserEvent;
180
+ }
181
+ switch (type) {
182
+ case connect:
183
+ return EventType.Connect;
184
+ case connected:
185
+ return EventType.Connected;
186
+ case disconnectd:
187
+ return EventType.Disconnected;
188
+ default:
189
+ return undefined;
190
+ }
191
+ }
192
+ function isWebPubSubRequest(req) {
193
+ return getHttpHeader(req, "ce-awpsversion") !== undefined;
194
+ }
195
+ /**
196
+ * @internal
197
+ */
198
+ class CloudEventsDispatcher {
199
+ constructor(hub, eventHandler) {
200
+ this.hub = hub;
201
+ this.eventHandler = eventHandler;
202
+ this._allowAll = true;
203
+ this._allowedOrigins = [];
204
+ if (Array.isArray(eventHandler)) {
205
+ throw new Error("Unexpected WebPubSubEventHandlerOptions");
206
+ }
207
+ if ((eventHandler === null || eventHandler === void 0 ? void 0 : eventHandler.allowedEndpoints) !== undefined) {
208
+ this._allowedOrigins = eventHandler.allowedEndpoints.map((endpoint) => new url.URL(endpoint).host.toLowerCase());
209
+ this._allowAll = false;
210
+ }
211
+ }
212
+ handlePreflight(req, res) {
213
+ var _a;
214
+ if (!isWebPubSubRequest(req)) {
215
+ return false;
216
+ }
217
+ const origin = (_a = getHttpHeader(req, "webhook-request-origin")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
218
+ if (origin === undefined) {
219
+ logger.warning("Expecting webhook-request-origin header.");
220
+ res.statusCode = 400;
221
+ }
222
+ else if (this._allowAll || this._allowedOrigins.indexOf(origin) > -1) {
223
+ res.setHeader("WebHook-Allowed-Origin", origin);
224
+ }
225
+ else {
226
+ logger.warning("Origin does not match the allowed origins: " + this._allowedOrigins);
227
+ res.statusCode = 400;
228
+ }
229
+ res.end();
230
+ return true;
231
+ }
232
+ async handleRequest(request, response) {
233
+ var _a, _b, _c, _d, _e;
234
+ if (!isWebPubSubRequest(request)) {
235
+ return false;
236
+ }
237
+ // check if it is a valid WebPubSub cloud events
238
+ const origin = getHttpHeader(request, "webhook-request-origin");
239
+ if (origin === undefined) {
240
+ return false;
241
+ }
242
+ const eventType = tryGetWebPubSubEvent(request);
243
+ if (eventType === undefined) {
244
+ return false;
245
+ }
246
+ // check if hub matches
247
+ const hub = getHttpHeader(request, "ce-hub");
248
+ if (hub !== this.hub) {
249
+ return false;
250
+ }
251
+ // No need to read body if handler is not specified
252
+ switch (eventType) {
253
+ case EventType.Connect:
254
+ if (!((_a = this.eventHandler) === null || _a === void 0 ? void 0 : _a.handleConnect)) {
255
+ response.end();
103
256
  return true;
104
257
  }
105
- case EventType.Connected: {
106
- // for unblocking events, we responds to the service as early as possible
258
+ break;
259
+ case EventType.Connected:
260
+ if (!((_b = this.eventHandler) === null || _b === void 0 ? void 0 : _b.onConnected)) {
107
261
  response.end();
108
- const connectedRequest = receivedEvent.data;
109
- connectedRequest.context = this.GetContext(receivedEvent, origin);
110
- this.eventHandler.onConnected(connectedRequest);
111
262
  return true;
112
263
  }
113
- case EventType.Disconnected: {
114
- // for unblocking events, we responds to the service as early as possible
264
+ break;
265
+ case EventType.Disconnected:
266
+ if (!((_c = this.eventHandler) === null || _c === void 0 ? void 0 : _c.onDisconnected)) {
115
267
  response.end();
116
- const disconnectedRequest = receivedEvent.data;
117
- disconnectedRequest.context = this.GetContext(receivedEvent, origin);
118
- this.eventHandler.onDisconnected(disconnectedRequest);
119
268
  return true;
120
269
  }
121
- case EventType.UserEvent: {
122
- let userRequest;
123
- if (receivedEvent.data_base64 !== undefined) {
124
- userRequest = {
125
- context: this.GetContext(receivedEvent, origin),
126
- data: Buffer.from(receivedEvent.data_base64, "base64"),
127
- dataType: "binary"
128
- };
129
- }
130
- else if (receivedEvent.data !== undefined) {
131
- userRequest = {
132
- context: this.GetContext(receivedEvent, origin),
133
- data: receivedEvent.data,
134
- dataType: ((_e = receivedEvent.datacontenttype) === null || _e === void 0 ? void 0 : _e.startsWith("application/json;"))
135
- ? "json"
136
- : "text"
137
- };
138
- }
139
- else {
140
- throw new Error("Unexpected data.");
141
- }
142
- this.eventHandler.handleUserEvent(userRequest, {
143
- success(data, dataType) {
144
- response.statusCode = 200;
145
- switch (dataType) {
146
- case "json":
147
- response.setHeader("Content-Type", "application/json; charset=utf-8");
148
- break;
149
- case "text":
150
- response.setHeader("Content-Type", "text/plain; charset=utf-8");
151
- break;
152
- default:
153
- response.setHeader("Content-Type", "application/octet-stream");
154
- break;
155
- }
156
- response.end(data !== null && data !== void 0 ? data : "");
157
- },
158
- fail(code, detail) {
159
- response.statusCode = code;
160
- response.end(detail !== null && detail !== void 0 ? detail : "");
161
- }
162
- });
270
+ break;
271
+ case EventType.UserEvent:
272
+ if (!((_d = this.eventHandler) === null || _d === void 0 ? void 0 : _d.handleUserEvent)) {
273
+ response.end();
163
274
  return true;
164
275
  }
165
- default:
166
- console.warn(`Unknown EventType ${eventType}`);
167
- return false;
168
- }
169
- });
170
- }
171
- tryGetWebPubSubEvent(req) {
172
- // check ce-type to see if it is a valid WebPubSub CloudEvent request
173
- const prefix = "azure.webpubsub.";
174
- const connect = "azure.webpubsub.sys.connect";
175
- const connected = "azure.webpubsub.sys.connected";
176
- const disconnectd = "azure.webpubsub.sys.disconnected";
177
- const userPrefix = "azure.webpubsub.user.";
178
- const type = this.getSingleHeader(req, "ce-type");
179
- if (!(type === null || type === void 0 ? void 0 : type.startsWith(prefix))) {
180
- return undefined;
181
- }
182
- if (type.startsWith(userPrefix)) {
183
- return EventType.UserEvent;
184
- }
185
- switch (type) {
186
- case connect:
187
- return EventType.Connect;
188
- case connected:
189
- return EventType.Connected;
190
- case disconnectd:
191
- return EventType.Disconnected;
276
+ break;
192
277
  default:
193
- return undefined;
194
- }
195
- }
196
- getSingleHeader(req, key) {
197
- const value = req.headers[key];
198
- if (value === undefined) {
199
- return undefined;
200
- }
201
- if (typeof value === "string") {
202
- return value;
278
+ logger.warning(`Unknown EventType ${eventType}`);
279
+ return false;
203
280
  }
204
- return value[0];
205
- }
206
- GetContext(ce, origin) {
207
- const context = {
208
- signature: ce["signature"],
209
- userId: ce["userid"],
210
- hub: ce["hub"],
211
- connectionId: ce["connectionid"],
212
- eventName: ce["eventname"],
213
- origin: origin
214
- };
215
- // TODO: validation
216
- return context;
217
- }
218
- convertHttpToEvent(request) {
219
- return tslib.__awaiter(this, void 0, void 0, function* () {
220
- const normalized = {
221
- headers: {},
222
- body: ""
223
- };
224
- if (request.headers) {
225
- for (const key in request.headers) {
226
- if (Object.prototype.hasOwnProperty.call(request.headers, key)) {
227
- const element = request.headers[key];
228
- if (element === undefined) {
229
- continue;
230
- }
231
- if (typeof element === "string") {
232
- normalized.headers[key] = element;
233
- }
234
- else {
235
- normalized.headers[key] = element.join(",");
236
- }
237
- }
281
+ const eventRequest = await convertHttpToEvent(request);
282
+ const receivedEvent = cloudevents.HTTP.toEvent(eventRequest);
283
+ logger.verbose(receivedEvent);
284
+ switch (eventType) {
285
+ case EventType.Connect: {
286
+ const connectRequest = receivedEvent.data;
287
+ connectRequest.context = getContext(receivedEvent, origin);
288
+ this.eventHandler.handleConnect(connectRequest, getConnectResponseHandler(connectRequest, response));
289
+ return true;
290
+ }
291
+ case EventType.Connected: {
292
+ // for unblocking events, we responds to the service as early as possible
293
+ response.end();
294
+ const connectedRequest = receivedEvent.data;
295
+ connectedRequest.context = getContext(receivedEvent, origin);
296
+ this.eventHandler.onConnected(connectedRequest);
297
+ return true;
298
+ }
299
+ case EventType.Disconnected: {
300
+ // for unblocking events, we responds to the service as early as possible
301
+ response.end();
302
+ const disconnectedRequest = receivedEvent.data;
303
+ disconnectedRequest.context = getContext(receivedEvent, origin);
304
+ this.eventHandler.onDisconnected(disconnectedRequest);
305
+ return true;
306
+ }
307
+ case EventType.UserEvent: {
308
+ let userRequest;
309
+ if (receivedEvent.data_base64 !== undefined) {
310
+ userRequest = {
311
+ context: getContext(receivedEvent, origin),
312
+ data: Buffer.from(receivedEvent.data_base64, "base64"),
313
+ dataType: "binary"
314
+ };
315
+ }
316
+ else if (receivedEvent.data !== undefined) {
317
+ userRequest = {
318
+ context: getContext(receivedEvent, origin),
319
+ data: receivedEvent.data,
320
+ dataType: ((_e = receivedEvent.datacontenttype) === null || _e === void 0 ? void 0 : _e.startsWith("application/json;"))
321
+ ? "json"
322
+ : "text"
323
+ };
238
324
  }
325
+ else {
326
+ throw new Error("Unexpected data.");
327
+ }
328
+ this.eventHandler.handleUserEvent(userRequest, getUserEventResponseHandler(userRequest, response));
329
+ return true;
239
330
  }
240
- normalized.body = yield this.readRequestBody(request);
241
- return normalized;
242
- });
243
- }
244
- readRequestBody(req) {
245
- return new Promise(function (resolve, reject) {
246
- const chunks = [];
247
- req.on("data", function (chunk) {
248
- chunks.push(chunk);
249
- });
250
- req.on("end", function () {
251
- const buffer = Buffer.concat(chunks);
252
- resolve(buffer.toString());
253
- });
254
- // reject on request error
255
- req.on("error", function (err) {
256
- // This is not a "Second reject", just a different sort of failure
257
- reject(err);
258
- });
259
- });
331
+ default:
332
+ logger.warning(`Unknown EventType ${eventType}`);
333
+ return false;
334
+ }
260
335
  }
261
336
  }
262
- var EventType;
263
- (function (EventType) {
264
- EventType[EventType["Connect"] = 0] = "Connect";
265
- EventType[EventType["Connected"] = 1] = "Connected";
266
- EventType[EventType["Disconnected"] = 2] = "Disconnected";
267
- EventType[EventType["UserEvent"] = 3] = "UserEvent";
268
- })(EventType || (EventType = {}));
269
337
 
270
338
  // Copyright (c) Microsoft Corporation.
271
339
  /**
@@ -280,7 +348,7 @@ class WebPubSubEventHandler {
280
348
  * import express from "express";
281
349
  * import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
282
350
  * const endpoint = "https://xxxx.webpubsubdev.azure.com"
283
- * const handler = new WebPubSubEventHandler('chat', [ endpoint ] {
351
+ * const handler = new WebPubSubEventHandler('chat', {
284
352
  * handleConnect: (req, res) => {
285
353
  * console.log(JSON.stringify(req));
286
354
  * return {};
@@ -292,39 +360,39 @@ class WebPubSubEventHandler {
292
360
  * console.log(JSON.stringify(req));
293
361
  * res.success("Hey " + req.data, req.dataType);
294
362
  * };
363
+ * allowedEndpoints: [ endpoint ]
295
364
  * },
296
365
  * });
297
366
  * ```
298
367
  *
299
- * @param hub The name of the hub to listen to
300
- * @param allowedEndpoints The allowed endpoints for the incoming CloudEvents request
301
- * @param options Options to configure the event handler
368
+ * @param hub - The name of the hub to listen to
369
+ * @param options - Options to configure the event handler
302
370
  */
303
- constructor(hub, allowedEndpoints, options) {
371
+ constructor(hub, options) {
304
372
  var _a;
305
373
  this.hub = hub;
306
374
  const path = ((_a = options === null || options === void 0 ? void 0 : options.path) !== null && _a !== void 0 ? _a : `/api/webpubsub/hubs/${hub}/`).toLowerCase();
307
375
  this.path = path.endsWith("/") ? path : path + "/";
308
- this._cloudEventsHandler = new CloudEventsDispatcher(this.hub, allowedEndpoints, options);
376
+ this._cloudEventsHandler = new CloudEventsDispatcher(this.hub, options);
309
377
  }
310
378
  /**
311
379
  * Get the middleware to process the CloudEvents requests
312
380
  */
313
381
  getMiddleware() {
314
- return (req, res, next) => tslib.__awaiter(this, void 0, void 0, function* () {
382
+ return async (req, res, next) => {
315
383
  // Request originalUrl can contain query while baseUrl + path not
316
384
  let requestUrl = (req.baseUrl + req.path).toLowerCase();
317
385
  // normalize the Url
318
386
  requestUrl = requestUrl.endsWith("/") ? requestUrl : requestUrl + "/";
319
- if (requestUrl === this.path) {
387
+ if (requestUrl.startsWith(this.path)) {
320
388
  if (req.method === "OPTIONS") {
321
- if (this._cloudEventsHandler.processValidateRequest(req, res)) {
389
+ if (this._cloudEventsHandler.handlePreflight(req, res)) {
322
390
  return;
323
391
  }
324
392
  }
325
393
  else if (req.method === "POST") {
326
394
  try {
327
- if (yield this._cloudEventsHandler.processRequest(req, res)) {
395
+ if (await this._cloudEventsHandler.handleRequest(req, res)) {
328
396
  return;
329
397
  }
330
398
  }
@@ -335,7 +403,7 @@ class WebPubSubEventHandler {
335
403
  }
336
404
  }
337
405
  next();
338
- });
406
+ };
339
407
  }
340
408
  }
341
409