@backstage/plugin-signals-backend 0.3.20-next.1 → 0.3.20-next.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @backstage/plugin-signals-backend
2
2
 
3
+ ## 0.3.20-next.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 3873113: The signals WebSocket endpoint now requires a valid user identity token. The token can appear anywhere in the requested WebSocket protocols. Connections without a token, with an invalid token, or with a non-user (service) token are rejected, and unauthenticated guest connections are no longer accepted.
8
+
3
9
  ## 0.3.20-next.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -7,3 +7,11 @@ Signals plugin allows backend plugins to publish messages to frontend plugins.
7
7
  ## Getting started
8
8
 
9
9
  To install this signals backend plugin, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section.
10
+
11
+ ## WebSocket authentication
12
+
13
+ The signals WebSocket endpoint requires a Backstage user token. The bundled
14
+ signals client sends the token in the `Sec-WebSocket-Protocol` header. Custom
15
+ clients can include the token alongside other protocol values; the backend
16
+ authenticates each value and selects the user token for the WebSocket handshake.
17
+ Connections without a valid user token are rejected.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { eventsServiceRef } from '@backstage/plugin-events-node';\nimport { createRouter } from './service/router';\n\n/**\n * Signals backend plugin\n *\n * @public\n */\nexport const signalsPlugin = createBackendPlugin({\n pluginId: 'signals',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n lifecycle: coreServices.rootLifecycle,\n discovery: coreServices.discovery,\n userInfo: coreServices.userInfo,\n auth: coreServices.auth,\n events: eventsServiceRef,\n instanceMetadata: coreServices.rootInstanceMetadata,\n },\n async init({\n httpRouter,\n logger,\n config,\n lifecycle,\n discovery,\n userInfo,\n auth,\n events,\n instanceMetadata,\n }) {\n httpRouter.use(\n await createRouter({\n logger,\n config,\n lifecycle,\n discovery,\n userInfo,\n auth,\n events,\n instanceMetadata,\n }),\n );\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","eventsServiceRef","createRouter"],"mappings":";;;;;;AA4BO,MAAM,gBAAgBA,oCAAA,CAAoB;AAAA,EAC/C,QAAA,EAAU,SAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,WAAWA,6BAAA,CAAa,aAAA;AAAA,QACxB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,MAAA,EAAQC,iCAAA;AAAA,QACR,kBAAkBD,6BAAA,CAAa;AAAA,OACjC;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,SAAA;AAAA,QACA,SAAA;AAAA,QACA,QAAA;AAAA,QACA,IAAA;AAAA,QACA,MAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAME,mBAAA,CAAa;AAAA,YACjB,MAAA;AAAA,YACA,MAAA;AAAA,YACA,SAAA;AAAA,YACA,SAAA;AAAA,YACA,QAAA;AAAA,YACA,IAAA;AAAA,YACA,MAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AACA,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { eventsServiceRef } from '@backstage/plugin-events-node';\nimport { createRouter } from './service/router';\n\n/**\n * Signals backend plugin\n *\n * @public\n */\nexport const signalsPlugin = createBackendPlugin({\n pluginId: 'signals',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n lifecycle: coreServices.rootLifecycle,\n discovery: coreServices.discovery,\n userInfo: coreServices.userInfo,\n auth: coreServices.auth,\n events: eventsServiceRef,\n instanceMetadata: coreServices.rootInstanceMetadata,\n },\n async init({\n httpRouter,\n logger,\n config,\n lifecycle,\n discovery,\n userInfo,\n auth,\n events,\n instanceMetadata,\n }) {\n httpRouter.use(\n await createRouter({\n logger,\n config,\n lifecycle,\n discovery,\n userInfo,\n auth,\n events,\n instanceMetadata,\n }),\n );\n // Browser WebSockets cannot send an Authorization header, so the\n // credentials barrier must allow the handshake through. Authentication\n // is enforced in the WebSocket upgrade handler via Sec-WebSocket-Protocol.\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","eventsServiceRef","createRouter"],"mappings":";;;;;;AA4BO,MAAM,gBAAgBA,oCAAA,CAAoB;AAAA,EAC/C,QAAA,EAAU,SAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,WAAWA,6BAAA,CAAa,aAAA;AAAA,QACxB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,MAAA,EAAQC,iCAAA;AAAA,QACR,kBAAkBD,6BAAA,CAAa;AAAA,OACjC;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,SAAA;AAAA,QACA,SAAA;AAAA,QACA,QAAA;AAAA,QACA,IAAA;AAAA,QACA,MAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAME,mBAAA,CAAa;AAAA,YACjB,MAAA;AAAA,YACA,MAAA;AAAA,YACA,SAAA;AAAA,YACA,SAAA;AAAA,YACA,QAAA;AAAA,YACA,IAAA;AAAA,YACA,MAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAIA,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}
@@ -50,11 +50,9 @@ class SignalManager {
50
50
  const id = node_crypto.randomUUID();
51
51
  const conn = {
52
52
  id,
53
- user: identity?.userEntityRef ?? "user:default/guest",
53
+ user: identity.userEntityRef,
54
54
  ws,
55
- ownershipEntityRefs: identity?.ownershipEntityRefs ?? [
56
- "user:default/guest"
57
- ],
55
+ ownershipEntityRefs: identity.ownershipEntityRefs,
58
56
  subscriptions: /* @__PURE__ */ new Set(),
59
57
  isAlive: true
60
58
  };
@@ -1 +1 @@
1
- {"version":3,"file":"SignalManager.cjs.js","sources":["../../src/service/SignalManager.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EventParams, EventsService } from '@backstage/plugin-events-node';\nimport { SignalPayload } from '@backstage/plugin-signals-node';\nimport { RawData, WebSocket } from 'ws';\nimport { randomUUID as uuid } from 'node:crypto';\nimport { JsonObject } from '@backstage/types';\nimport {\n BackstageUserInfo,\n LifecycleService,\n LoggerService,\n RootInstanceMetadataService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\n\n/**\n * @internal\n */\nexport type SignalConnection = {\n id: string;\n user: string;\n ws: WebSocket;\n ownershipEntityRefs: string[];\n subscriptions: Set<string>;\n isAlive: boolean;\n};\n\n/**\n * @internal\n */\nexport type SignalManagerOptions = {\n events: EventsService;\n config: Config;\n logger: LoggerService;\n lifecycle: LifecycleService;\n instanceMetadata: RootInstanceMetadataService;\n};\n\n/** @internal */\nexport class SignalManager {\n private connections: Map<string, SignalConnection> = new Map<\n string,\n SignalConnection\n >();\n private events: EventsService;\n private logger: LoggerService;\n private pingInterval: ReturnType<typeof setInterval> | undefined;\n\n static create(options: SignalManagerOptions) {\n return new SignalManager(options);\n }\n\n private constructor(options: SignalManagerOptions) {\n this.events = options.events;\n\n // Use a unique subscriber ID for each signals instance, in order to fan-out\n // all events to each signals instance. This ensures that events always\n // reach users in a scaled deployment.\n const id = options.instanceMetadata.getId();\n this.logger = options.logger.child({ subscriberId: id });\n this.logger.info(`Signals manager is subscribing to signals events`);\n\n this.events.subscribe({\n id,\n topics: ['signals'],\n onEvent: (params: EventParams) =>\n this.onEventBrokerEvent(params.eventPayload as SignalPayload),\n });\n\n options.lifecycle.addShutdownHook(() => this.onShutdown());\n }\n\n private ping() {\n this.connections.forEach(conn => {\n if (!conn.isAlive) {\n this.logger.debug(`Connection ${conn.id} is not alive, terminating`);\n conn.ws.terminate();\n return;\n }\n\n conn.isAlive = false;\n conn.ws.ping();\n });\n }\n\n private onShutdown() {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n }\n\n // TODO: Unsubscribe from events?\n\n this.connections.forEach(conn => {\n conn.ws.terminate();\n });\n this.connections.clear();\n }\n\n addConnection(ws: WebSocket, identity?: BackstageUserInfo) {\n // Start pinging on first connection\n if (!this.pingInterval) {\n this.pingInterval = setInterval(() => this.ping(), 30000);\n }\n\n const id = uuid();\n const conn = {\n id,\n user: identity?.userEntityRef ?? 'user:default/guest',\n ws,\n ownershipEntityRefs: identity?.ownershipEntityRefs ?? [\n 'user:default/guest',\n ],\n subscriptions: new Set<string>(),\n isAlive: true,\n };\n\n this.connections.set(id, conn);\n\n this.logger.debug(`Connection ${id} connected`);\n ws.on('error', (err: Error) => {\n this.logger.error(\n `Error occurred with connection ${id}: ${err}, closing connection`,\n );\n ws.terminate();\n this.connections.delete(id);\n });\n\n ws.on('close', (code: number, reason: Buffer) => {\n this.logger.debug(\n `Connection ${id} closed with code ${code}, reason: ${reason}`,\n );\n ws.terminate();\n this.connections.delete(id);\n });\n\n ws.on('ping', () => {\n conn.isAlive = true;\n ws.pong();\n });\n\n ws.on('pong', () => {\n conn.isAlive = true;\n });\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n this.logger.debug(`Received message from connection ${id}: ${data}`);\n if (isBinary) {\n return;\n }\n try {\n const json = JSON.parse(data.toString()) as JsonObject;\n this.handleMessage(conn, json);\n } catch (err: any) {\n this.logger.error(\n `Invalid message received from connection ${id}: ${err}`,\n );\n }\n });\n }\n\n private handleMessage(connection: SignalConnection, message: JsonObject) {\n if (message.action === 'subscribe' && message.channel) {\n this.logger.debug(\n `Connection ${connection.id} subscribed to ${message.channel}`,\n );\n connection.subscriptions.add(message.channel as string);\n } else if (message.action === 'unsubscribe' && message.channel) {\n this.logger.debug(\n `Connection ${connection.id} unsubscribed from ${message.channel}`,\n );\n connection.subscriptions.delete(message.channel as string);\n }\n }\n\n private async onEventBrokerEvent(eventPayload: SignalPayload): Promise<void> {\n if (!eventPayload.channel || !eventPayload.message) {\n return;\n }\n\n const { channel, recipients, message } = eventPayload;\n const jsonMessage = JSON.stringify({ channel, message });\n let users: string[] = [];\n if (recipients.type === 'user') {\n users = Array.isArray(recipients.entityRef)\n ? recipients.entityRef\n : [recipients.entityRef];\n }\n\n // Actual websocket message sending\n this.connections.forEach(conn => {\n if (!conn.subscriptions.has(channel)) {\n return;\n }\n\n // Sending to all users can be done with broadcast\n if (\n recipients.type !== 'broadcast' &&\n !conn.ownershipEntityRefs.some((ref: string) => users.includes(ref))\n ) {\n return;\n }\n\n if (conn.ws.readyState !== WebSocket.OPEN) {\n return;\n }\n\n conn.ws.send(jsonMessage, err => {\n if (err) {\n this.logger.error(`Failed to send message to ${conn.id}: ${err}`);\n }\n });\n });\n }\n}\n"],"names":["uuid","WebSocket"],"mappings":"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"}
1
+ {"version":3,"file":"SignalManager.cjs.js","sources":["../../src/service/SignalManager.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EventParams, EventsService } from '@backstage/plugin-events-node';\nimport { SignalPayload } from '@backstage/plugin-signals-node';\nimport { RawData, WebSocket } from 'ws';\nimport { randomUUID as uuid } from 'node:crypto';\nimport { JsonObject } from '@backstage/types';\nimport {\n BackstageUserInfo,\n LifecycleService,\n LoggerService,\n RootInstanceMetadataService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\n\n/**\n * @internal\n */\nexport type SignalConnection = {\n id: string;\n user: string;\n ws: WebSocket;\n ownershipEntityRefs: string[];\n subscriptions: Set<string>;\n isAlive: boolean;\n};\n\n/**\n * @internal\n */\nexport type SignalManagerOptions = {\n events: EventsService;\n config: Config;\n logger: LoggerService;\n lifecycle: LifecycleService;\n instanceMetadata: RootInstanceMetadataService;\n};\n\n/** @internal */\nexport class SignalManager {\n private connections: Map<string, SignalConnection> = new Map<\n string,\n SignalConnection\n >();\n private events: EventsService;\n private logger: LoggerService;\n private pingInterval: ReturnType<typeof setInterval> | undefined;\n\n static create(options: SignalManagerOptions) {\n return new SignalManager(options);\n }\n\n private constructor(options: SignalManagerOptions) {\n this.events = options.events;\n\n // Use a unique subscriber ID for each signals instance, in order to fan-out\n // all events to each signals instance. This ensures that events always\n // reach users in a scaled deployment.\n const id = options.instanceMetadata.getId();\n this.logger = options.logger.child({ subscriberId: id });\n this.logger.info(`Signals manager is subscribing to signals events`);\n\n this.events.subscribe({\n id,\n topics: ['signals'],\n onEvent: (params: EventParams) =>\n this.onEventBrokerEvent(params.eventPayload as SignalPayload),\n });\n\n options.lifecycle.addShutdownHook(() => this.onShutdown());\n }\n\n private ping() {\n this.connections.forEach(conn => {\n if (!conn.isAlive) {\n this.logger.debug(`Connection ${conn.id} is not alive, terminating`);\n conn.ws.terminate();\n return;\n }\n\n conn.isAlive = false;\n conn.ws.ping();\n });\n }\n\n private onShutdown() {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n }\n\n // TODO: Unsubscribe from events?\n\n this.connections.forEach(conn => {\n conn.ws.terminate();\n });\n this.connections.clear();\n }\n\n addConnection(ws: WebSocket, identity: BackstageUserInfo) {\n // Start pinging on first connection\n if (!this.pingInterval) {\n this.pingInterval = setInterval(() => this.ping(), 30000);\n }\n\n const id = uuid();\n const conn = {\n id,\n user: identity.userEntityRef,\n ws,\n ownershipEntityRefs: identity.ownershipEntityRefs,\n subscriptions: new Set<string>(),\n isAlive: true,\n };\n\n this.connections.set(id, conn);\n\n this.logger.debug(`Connection ${id} connected`);\n ws.on('error', (err: Error) => {\n this.logger.error(\n `Error occurred with connection ${id}: ${err}, closing connection`,\n );\n ws.terminate();\n this.connections.delete(id);\n });\n\n ws.on('close', (code: number, reason: Buffer) => {\n this.logger.debug(\n `Connection ${id} closed with code ${code}, reason: ${reason}`,\n );\n ws.terminate();\n this.connections.delete(id);\n });\n\n ws.on('ping', () => {\n conn.isAlive = true;\n ws.pong();\n });\n\n ws.on('pong', () => {\n conn.isAlive = true;\n });\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n this.logger.debug(`Received message from connection ${id}: ${data}`);\n if (isBinary) {\n return;\n }\n try {\n const json = JSON.parse(data.toString()) as JsonObject;\n this.handleMessage(conn, json);\n } catch (err: any) {\n this.logger.error(\n `Invalid message received from connection ${id}: ${err}`,\n );\n }\n });\n }\n\n private handleMessage(connection: SignalConnection, message: JsonObject) {\n if (message.action === 'subscribe' && message.channel) {\n this.logger.debug(\n `Connection ${connection.id} subscribed to ${message.channel}`,\n );\n connection.subscriptions.add(message.channel as string);\n } else if (message.action === 'unsubscribe' && message.channel) {\n this.logger.debug(\n `Connection ${connection.id} unsubscribed from ${message.channel}`,\n );\n connection.subscriptions.delete(message.channel as string);\n }\n }\n\n private async onEventBrokerEvent(eventPayload: SignalPayload): Promise<void> {\n if (!eventPayload.channel || !eventPayload.message) {\n return;\n }\n\n const { channel, recipients, message } = eventPayload;\n const jsonMessage = JSON.stringify({ channel, message });\n let users: string[] = [];\n if (recipients.type === 'user') {\n users = Array.isArray(recipients.entityRef)\n ? recipients.entityRef\n : [recipients.entityRef];\n }\n\n // Actual websocket message sending\n this.connections.forEach(conn => {\n if (!conn.subscriptions.has(channel)) {\n return;\n }\n\n // Sending to all users can be done with broadcast\n if (\n recipients.type !== 'broadcast' &&\n !conn.ownershipEntityRefs.some((ref: string) => users.includes(ref))\n ) {\n return;\n }\n\n if (conn.ws.readyState !== WebSocket.OPEN) {\n return;\n }\n\n conn.ws.send(jsonMessage, err => {\n if (err) {\n this.logger.error(`Failed to send message to ${conn.id}: ${err}`);\n }\n });\n });\n }\n}\n"],"names":["uuid","WebSocket"],"mappings":"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"}
@@ -10,16 +10,38 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
10
10
  var express__default = /*#__PURE__*/_interopDefaultCompat(express);
11
11
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
12
12
 
13
+ function readWebSocketProtocols(header) {
14
+ return (Array.isArray(header) ? header : [header]).flatMap((value) => value?.split(",") ?? []).map((value) => value.trim()).filter(Boolean);
15
+ }
16
+ function rejectUpgrade(socket, statusLine, logger, details) {
17
+ logger.warn("WebSocket upgrade rejected", {
18
+ remoteAddress: details.remoteAddress,
19
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
20
+ reason: details.reason
21
+ });
22
+ socket.end(
23
+ `${statusLine}\r
24
+ Connection: close\r
25
+ Content-Length: 0\r
26
+ \r
27
+ `,
28
+ () => {
29
+ socket.destroy();
30
+ }
31
+ );
32
+ }
13
33
  async function createRouter(options) {
14
34
  const { logger, discovery, auth, userInfo } = options;
15
35
  const manager = SignalManager.SignalManager.create(options);
16
36
  let subscribedToUpgradeRequests = false;
17
37
  let apiUrl = void 0;
38
+ const authenticatedProtocols = /* @__PURE__ */ new WeakMap();
18
39
  const webSocketServer = new ws.WebSocketServer({
19
40
  noServer: true,
20
41
  // handle upgrade manually
21
- clientTracking: false
42
+ clientTracking: false,
22
43
  // handle connections in SignalManager
44
+ handleProtocols: (_, request) => authenticatedProtocols.get(request) ?? false
23
45
  });
24
46
  webSocketServer.on("error", (error) => {
25
47
  logger.error(`WebSocket server error: ${error}`);
@@ -31,24 +53,46 @@ async function createRouter(options) {
31
53
  if (!request.url || !apiUrl || !apiUrl.endsWith(request.url)) {
32
54
  return;
33
55
  }
34
- let userIdentity = void 0;
35
- try {
36
- const token = request.headers["sec-websocket-protocol"];
37
- if (token) {
38
- const credentials = await auth.authenticate(token);
56
+ const protocols = readWebSocketProtocols(
57
+ request.headers["sec-websocket-protocol"]
58
+ );
59
+ if (protocols.length === 0) {
60
+ rejectUpgrade(socket, "HTTP/1.1 401 Unauthorized", logger, {
61
+ remoteAddress: request.socket?.remoteAddress,
62
+ reason: "missing_token"
63
+ });
64
+ return;
65
+ }
66
+ let userIdentity;
67
+ let authenticatedProtocol;
68
+ let rejectionReason = "invalid_token";
69
+ for (const protocol of protocols) {
70
+ try {
71
+ const credentials = await auth.authenticate(protocol);
39
72
  if (auth.isPrincipal(credentials, "user")) {
40
73
  userIdentity = await userInfo.getUserInfo(credentials);
74
+ authenticatedProtocol = protocol;
75
+ break;
41
76
  }
77
+ rejectionReason = "non_user_principal";
78
+ } catch (e) {
79
+ logger.debug("WebSocket authentication failed", {
80
+ remoteAddress: request.socket?.remoteAddress,
81
+ reason: "invalid_token",
82
+ errorName: e instanceof Error ? e.name : void 0,
83
+ errorMessage: e instanceof Error ? e.message : String(e)
84
+ });
42
85
  }
43
- } catch (e) {
44
- logger.error(`Failed to authenticate WebSocket connection: ${e}`);
45
- socket.end(
46
- "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
47
- () => socket.destroy()
48
- );
86
+ }
87
+ if (!userIdentity || !authenticatedProtocol) {
88
+ rejectUpgrade(socket, "HTTP/1.1 401 Unauthorized", logger, {
89
+ remoteAddress: request.socket?.remoteAddress,
90
+ reason: rejectionReason
91
+ });
49
92
  return;
50
93
  }
51
94
  try {
95
+ authenticatedProtocols.set(request, authenticatedProtocol);
52
96
  webSocketServer.handleUpgrade(
53
97
  request,
54
98
  socket,
@@ -59,20 +103,21 @@ async function createRouter(options) {
59
103
  );
60
104
  } catch (e) {
61
105
  logger.error(`Failed to handle WebSocket upgrade: ${e}`);
62
- socket.end(
63
- "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
64
- () => socket.destroy()
65
- );
106
+ rejectUpgrade(socket, "HTTP/1.1 500 Internal Server Error", logger, {
107
+ remoteAddress: request.socket?.remoteAddress,
108
+ reason: "upgrade_failed"
109
+ });
66
110
  }
67
111
  };
68
112
  const upgradeMiddleware = async (req, _, next) => {
69
- const server = req.socket?.server;
70
- if (subscribedToUpgradeRequests || !server || !req.headers || req.headers.upgrade === void 0 || req.headers.upgrade.toLowerCase() !== "websocket") {
71
- next();
72
- return;
113
+ if (!subscribedToUpgradeRequests) {
114
+ const server = req.socket?.server;
115
+ if (server) {
116
+ subscribedToUpgradeRequests = true;
117
+ server.on("upgrade", handleUpgrade);
118
+ }
73
119
  }
74
- subscribedToUpgradeRequests = true;
75
- server.on("upgrade", handleUpgrade);
120
+ next();
76
121
  };
77
122
  const router = Router__default.default();
78
123
  router.use(express__default.default.json());
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express, { NextFunction, Request, Response } from 'express';\nimport Router from 'express-promise-router';\nimport {\n AuthService,\n BackstageUserInfo,\n DiscoveryService,\n LifecycleService,\n LoggerService,\n RootInstanceMetadataService,\n UserInfoService,\n} from '@backstage/backend-plugin-api';\nimport * as https from 'node:https';\nimport http, { IncomingMessage } from 'node:http';\nimport { SignalManager } from './SignalManager';\nimport { EventsService } from '@backstage/plugin-events-node';\nimport { WebSocket, WebSocketServer } from 'ws';\nimport { Duplex } from 'node:stream';\nimport { Config } from '@backstage/config';\n\nexport interface RouterOptions {\n logger: LoggerService;\n events: EventsService;\n discovery: DiscoveryService;\n config: Config;\n lifecycle: LifecycleService;\n instanceMetadata: RootInstanceMetadataService;\n userInfo: UserInfoService;\n auth: AuthService;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, discovery, auth, userInfo } = options;\n\n const manager = SignalManager.create(options);\n let subscribedToUpgradeRequests = false;\n let apiUrl: string | undefined = undefined;\n\n const webSocketServer = new WebSocketServer({\n noServer: true, // handle upgrade manually\n clientTracking: false, // handle connections in SignalManager\n });\n\n webSocketServer.on('error', (error: Error) => {\n logger.error(`WebSocket server error: ${error}`);\n });\n\n const handleUpgrade = async (\n request: Request<any, any, any, any, any>,\n socket: Duplex,\n head: Buffer,\n ) => {\n if (!apiUrl) {\n apiUrl = await discovery.getBaseUrl('signals');\n }\n\n if (!request.url || !apiUrl || !apiUrl.endsWith(request.url)) {\n return;\n }\n\n let userIdentity: BackstageUserInfo | undefined = undefined;\n\n // Authentication token is passed in Sec-WebSocket-Protocol header as there\n // is no other way to pass the token with plain websockets\n try {\n const token = request.headers['sec-websocket-protocol'];\n if (token) {\n const credentials = await auth.authenticate(token);\n if (auth.isPrincipal(credentials, 'user')) {\n userIdentity = await userInfo.getUserInfo(credentials);\n }\n }\n } catch (e) {\n logger.error(`Failed to authenticate WebSocket connection: ${e}`);\n socket.end(\n 'HTTP/1.1 401 Unauthorized\\r\\n' +\n 'Content-Length: 0\\r\\n' +\n 'Connection: close\\r\\n' +\n '\\r\\n',\n () => socket.destroy(),\n );\n return;\n }\n\n try {\n webSocketServer.handleUpgrade(\n request,\n socket,\n head,\n (ws: WebSocket, __: IncomingMessage) => {\n manager.addConnection(ws, userIdentity);\n },\n );\n } catch (e) {\n logger.error(`Failed to handle WebSocket upgrade: ${e}`);\n socket.end(\n 'HTTP/1.1 500 Internal Server Error\\r\\n' +\n 'Content-Length: 0\\r\\n' +\n 'Connection: close\\r\\n' +\n '\\r\\n',\n () => socket.destroy(),\n );\n }\n };\n\n const upgradeMiddleware = async (\n req: Request,\n _: Response,\n next: NextFunction,\n ) => {\n const server: https.Server | http.Server = (req.socket as any)?.server;\n if (\n subscribedToUpgradeRequests ||\n !server ||\n !req.headers ||\n req.headers.upgrade === undefined ||\n req.headers.upgrade.toLowerCase() !== 'websocket'\n ) {\n next();\n return;\n }\n\n subscribedToUpgradeRequests = true;\n server.on('upgrade', handleUpgrade);\n };\n\n const router = Router();\n router.use(express.json());\n router.use(upgradeMiddleware);\n\n router.get('/health', (_, response) => {\n response.json({ status: 'ok' });\n });\n\n return router;\n}\n"],"names":["SignalManager","WebSocketServer","Router","express"],"mappings":";;;;;;;;;;;;AA8CA,eAAsB,aACpB,OAAA,EACyB;AACzB,EAAA,MAAM,EAAE,MAAA,EAAQ,SAAA,EAAW,IAAA,EAAM,UAAS,GAAI,OAAA;AAE9C,EAAA,MAAM,OAAA,GAAUA,2BAAA,CAAc,MAAA,CAAO,OAAO,CAAA;AAC5C,EAAA,IAAI,2BAAA,GAA8B,KAAA;AAClC,EAAA,IAAI,MAAA,GAA6B,MAAA;AAEjC,EAAA,MAAM,eAAA,GAAkB,IAAIC,kBAAA,CAAgB;AAAA,IAC1C,QAAA,EAAU,IAAA;AAAA;AAAA,IACV,cAAA,EAAgB;AAAA;AAAA,GACjB,CAAA;AAED,EAAA,eAAA,CAAgB,EAAA,CAAG,OAAA,EAAS,CAAC,KAAA,KAAiB;AAC5C,IAAA,MAAA,CAAO,KAAA,CAAM,CAAA,wBAAA,EAA2B,KAAK,CAAA,CAAE,CAAA;AAAA,EACjD,CAAC,CAAA;AAED,EAAA,MAAM,aAAA,GAAgB,OACpB,OAAA,EACA,MAAA,EACA,IAAA,KACG;AACH,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,MAAA,GAAS,MAAM,SAAA,CAAU,UAAA,CAAW,SAAS,CAAA;AAAA,IAC/C;AAEA,IAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,IAAO,CAAC,MAAA,IAAU,CAAC,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,GAAG,CAAA,EAAG;AAC5D,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,YAAA,GAA8C,MAAA;AAIlD,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,wBAAwB,CAAA;AACtD,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,YAAA,CAAa,KAAK,CAAA;AACjD,QAAA,IAAI,IAAA,CAAK,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AACzC,UAAA,YAAA,GAAe,MAAM,QAAA,CAAS,WAAA,CAAY,WAAW,CAAA;AAAA,QACvD;AAAA,MACF;AAAA,IACF,SAAS,CAAA,EAAG;AACV,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,6CAAA,EAAgD,CAAC,CAAA,CAAE,CAAA;AAChE,MAAA,MAAA,CAAO,GAAA;AAAA,QACL,6EAAA;AAAA,QAIA,MAAM,OAAO,OAAA;AAAQ,OACvB;AACA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,eAAA,CAAgB,aAAA;AAAA,QACd,OAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA,CAAC,IAAe,EAAA,KAAwB;AACtC,UAAA,OAAA,CAAQ,aAAA,CAAc,IAAI,YAAY,CAAA;AAAA,QACxC;AAAA,OACF;AAAA,IACF,SAAS,CAAA,EAAG;AACV,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,oCAAA,EAAuC,CAAC,CAAA,CAAE,CAAA;AACvD,MAAA,MAAA,CAAO,GAAA;AAAA,QACL,sFAAA;AAAA,QAIA,MAAM,OAAO,OAAA;AAAQ,OACvB;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,iBAAA,GAAoB,OACxB,GAAA,EACA,CAAA,EACA,IAAA,KACG;AACH,IAAA,MAAM,MAAA,GAAsC,IAAI,MAAA,EAAgB,MAAA;AAChE,IAAA,IACE,2BAAA,IACA,CAAC,MAAA,IACD,CAAC,IAAI,OAAA,IACL,GAAA,CAAI,OAAA,CAAQ,OAAA,KAAY,UACxB,GAAA,CAAI,OAAA,CAAQ,OAAA,CAAQ,WAAA,OAAkB,WAAA,EACtC;AACA,MAAA,IAAA,EAAK;AACL,MAAA;AAAA,IACF;AAEA,IAAA,2BAAA,GAA8B,IAAA;AAC9B,IAAA,MAAA,CAAO,EAAA,CAAG,WAAW,aAAa,CAAA;AAAA,EACpC,CAAA;AAEA,EAAA,MAAM,SAASC,uBAAA,EAAO;AACtB,EAAA,MAAA,CAAO,GAAA,CAAIC,wBAAA,CAAQ,IAAA,EAAM,CAAA;AACzB,EAAA,MAAA,CAAO,IAAI,iBAAiB,CAAA;AAE5B,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,CAAC,CAAA,EAAG,QAAA,KAAa;AACrC,IAAA,QAAA,CAAS,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAChC,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express, { NextFunction, Request, Response } from 'express';\nimport Router from 'express-promise-router';\nimport {\n AuthService,\n BackstageUserInfo,\n DiscoveryService,\n LifecycleService,\n LoggerService,\n RootInstanceMetadataService,\n UserInfoService,\n} from '@backstage/backend-plugin-api';\nimport * as https from 'node:https';\nimport http, { IncomingMessage } from 'node:http';\nimport { SignalManager } from './SignalManager';\nimport { EventsService } from '@backstage/plugin-events-node';\nimport { WebSocket, WebSocketServer } from 'ws';\nimport { Duplex } from 'node:stream';\nimport { Config } from '@backstage/config';\n\nexport interface RouterOptions {\n logger: LoggerService;\n events: EventsService;\n discovery: DiscoveryService;\n config: Config;\n lifecycle: LifecycleService;\n instanceMetadata: RootInstanceMetadataService;\n userInfo: UserInfoService;\n auth: AuthService;\n}\n\nfunction readWebSocketProtocols(\n header: string | string[] | undefined,\n): string[] {\n return (Array.isArray(header) ? header : [header])\n .flatMap(value => value?.split(',') ?? [])\n .map(value => value.trim())\n .filter(Boolean);\n}\n\nfunction rejectUpgrade(\n socket: Duplex,\n statusLine: string,\n logger: LoggerService,\n details: {\n remoteAddress?: string;\n reason: string;\n },\n) {\n logger.warn('WebSocket upgrade rejected', {\n remoteAddress: details.remoteAddress,\n timestamp: new Date().toISOString(),\n reason: details.reason,\n });\n // Flush the HTTP response, then destroy so the socket cannot linger.\n socket.end(\n `${statusLine}\\r\\n` +\n 'Connection: close\\r\\n' +\n 'Content-Length: 0\\r\\n' +\n '\\r\\n',\n () => {\n socket.destroy();\n },\n );\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, discovery, auth, userInfo } = options;\n\n const manager = SignalManager.create(options);\n let subscribedToUpgradeRequests = false;\n let apiUrl: string | undefined = undefined;\n const authenticatedProtocols = new WeakMap<IncomingMessage, string>();\n\n const webSocketServer = new WebSocketServer({\n noServer: true, // handle upgrade manually\n clientTracking: false, // handle connections in SignalManager\n handleProtocols: (_, request) =>\n authenticatedProtocols.get(request) ?? false,\n });\n\n webSocketServer.on('error', (error: Error) => {\n logger.error(`WebSocket server error: ${error}`);\n });\n\n const handleUpgrade = async (\n request: Request<any, any, any, any, any>,\n socket: Duplex,\n head: Buffer,\n ) => {\n if (!apiUrl) {\n apiUrl = await discovery.getBaseUrl('signals');\n }\n\n if (!request.url || !apiUrl || !apiUrl.endsWith(request.url)) {\n return;\n }\n\n // Authentication token is passed in Sec-WebSocket-Protocol header as there\n // is no other way to pass the token with plain websockets\n const protocols = readWebSocketProtocols(\n request.headers['sec-websocket-protocol'],\n );\n if (protocols.length === 0) {\n rejectUpgrade(socket, 'HTTP/1.1 401 Unauthorized', logger, {\n remoteAddress: request.socket?.remoteAddress,\n reason: 'missing_token',\n });\n return;\n }\n\n let userIdentity: BackstageUserInfo | undefined;\n let authenticatedProtocol: string | undefined;\n let rejectionReason = 'invalid_token';\n for (const protocol of protocols) {\n try {\n const credentials = await auth.authenticate(protocol);\n if (auth.isPrincipal(credentials, 'user')) {\n userIdentity = await userInfo.getUserInfo(credentials);\n authenticatedProtocol = protocol;\n break;\n }\n rejectionReason = 'non_user_principal';\n } catch (e) {\n logger.debug('WebSocket authentication failed', {\n remoteAddress: request.socket?.remoteAddress,\n reason: 'invalid_token',\n errorName: e instanceof Error ? e.name : undefined,\n errorMessage: e instanceof Error ? e.message : String(e),\n });\n }\n }\n\n if (!userIdentity || !authenticatedProtocol) {\n rejectUpgrade(socket, 'HTTP/1.1 401 Unauthorized', logger, {\n remoteAddress: request.socket?.remoteAddress,\n reason: rejectionReason,\n });\n return;\n }\n\n try {\n authenticatedProtocols.set(request, authenticatedProtocol);\n webSocketServer.handleUpgrade(\n request,\n socket,\n head,\n (ws: WebSocket, __: IncomingMessage) => {\n manager.addConnection(ws, userIdentity);\n },\n );\n } catch (e) {\n logger.error(`Failed to handle WebSocket upgrade: ${e}`);\n rejectUpgrade(socket, 'HTTP/1.1 500 Internal Server Error', logger, {\n remoteAddress: request.socket?.remoteAddress,\n reason: 'upgrade_failed',\n });\n }\n };\n\n // The HTTP server is only available via the request socket, so the upgrade\n // listener is registered on the first request that reaches this router.\n // Until then, WebSocket upgrades for this plugin are not handled (same\n // constraint as before; registering on any first request is slightly more\n // reliable than waiting for an Upgrade request through Express).\n const upgradeMiddleware = async (\n req: Request,\n _: Response,\n next: NextFunction,\n ) => {\n if (!subscribedToUpgradeRequests) {\n const server: https.Server | http.Server = (req.socket as any)?.server;\n if (server) {\n subscribedToUpgradeRequests = true;\n server.on('upgrade', handleUpgrade);\n }\n }\n next();\n };\n\n const router = Router();\n router.use(express.json());\n router.use(upgradeMiddleware);\n\n router.get('/health', (_, response) => {\n response.json({ status: 'ok' });\n });\n\n return router;\n}\n"],"names":["SignalManager","WebSocketServer","Router","express"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-signals-backend",
3
- "version": "0.3.20-next.1",
3
+ "version": "0.3.20-next.2",
4
4
  "backstage": {
5
5
  "role": "backend-plugin",
6
6
  "pluginId": "signals",
@@ -50,12 +50,12 @@
50
50
  "ws": "^8.20.1"
51
51
  },
52
52
  "devDependencies": {
53
- "@backstage/backend-defaults": "0.18.1-next.1",
53
+ "@backstage/backend-defaults": "0.18.1-next.2",
54
54
  "@backstage/backend-test-utils": "1.11.8-next.1",
55
55
  "@backstage/cli": "0.36.7-next.0",
56
56
  "@backstage/plugin-auth-backend": "0.30.2-next.1",
57
57
  "@backstage/plugin-auth-backend-module-guest-provider": "0.2.24-next.1",
58
- "@backstage/plugin-events-backend": "0.6.7-next.1",
58
+ "@backstage/plugin-events-backend": "0.6.7-next.2",
59
59
  "@types/express": "^4.17.6",
60
60
  "@types/supertest": "^2.0.8",
61
61
  "@types/ws": "^8.5.10",