@backstage/plugin-signals-backend 0.3.20-next.0 → 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 +16 -0
- package/README.md +8 -0
- package/dist/plugin.cjs.js +6 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/service/SignalManager.cjs.js +5 -11
- package/dist/service/SignalManager.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +67 -22
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.3.20-next.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e956084: Use the backend instance ID to identify the event subscription, allowing deployments to control how Signals instances fan out events.
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @backstage/backend-plugin-api@1.11.0-next.1
|
|
16
|
+
- @backstage/plugin-events-node@0.4.27-next.1
|
|
17
|
+
- @backstage/plugin-signals-node@0.2.6-next.1
|
|
18
|
+
|
|
3
19
|
## 0.3.20-next.0
|
|
4
20
|
|
|
5
21
|
### 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.
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -16,7 +16,8 @@ const signalsPlugin = backendPluginApi.createBackendPlugin({
|
|
|
16
16
|
discovery: backendPluginApi.coreServices.discovery,
|
|
17
17
|
userInfo: backendPluginApi.coreServices.userInfo,
|
|
18
18
|
auth: backendPluginApi.coreServices.auth,
|
|
19
|
-
events: pluginEventsNode.eventsServiceRef
|
|
19
|
+
events: pluginEventsNode.eventsServiceRef,
|
|
20
|
+
instanceMetadata: backendPluginApi.coreServices.rootInstanceMetadata
|
|
20
21
|
},
|
|
21
22
|
async init({
|
|
22
23
|
httpRouter,
|
|
@@ -26,7 +27,8 @@ const signalsPlugin = backendPluginApi.createBackendPlugin({
|
|
|
26
27
|
discovery,
|
|
27
28
|
userInfo,
|
|
28
29
|
auth,
|
|
29
|
-
events
|
|
30
|
+
events,
|
|
31
|
+
instanceMetadata
|
|
30
32
|
}) {
|
|
31
33
|
httpRouter.use(
|
|
32
34
|
await router.createRouter({
|
|
@@ -36,7 +38,8 @@ const signalsPlugin = backendPluginApi.createBackendPlugin({
|
|
|
36
38
|
discovery,
|
|
37
39
|
userInfo,
|
|
38
40
|
auth,
|
|
39
|
-
events
|
|
41
|
+
events,
|
|
42
|
+
instanceMetadata
|
|
40
43
|
})
|
|
41
44
|
);
|
|
42
45
|
httpRouter.addAuthPolicy({
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -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 },\n async init({\n httpRouter,\n logger,\n config,\n lifecycle,\n discovery,\n userInfo,\n auth,\n events,\n }) {\n httpRouter.use(\n await createRouter({\n logger,\n config,\n lifecycle,\n discovery,\n userInfo,\n auth,\n events,\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;AAAA,
|
|
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;;"}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var crypto = require('node:crypto');
|
|
4
3
|
var ws = require('ws');
|
|
5
|
-
|
|
6
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
-
|
|
8
|
-
var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|
4
|
+
var node_crypto = require('node:crypto');
|
|
9
5
|
|
|
10
6
|
class SignalManager {
|
|
11
7
|
connections = /* @__PURE__ */ new Map();
|
|
@@ -17,7 +13,7 @@ class SignalManager {
|
|
|
17
13
|
}
|
|
18
14
|
constructor(options) {
|
|
19
15
|
this.events = options.events;
|
|
20
|
-
const id =
|
|
16
|
+
const id = options.instanceMetadata.getId();
|
|
21
17
|
this.logger = options.logger.child({ subscriberId: id });
|
|
22
18
|
this.logger.info(`Signals manager is subscribing to signals events`);
|
|
23
19
|
this.events.subscribe({
|
|
@@ -51,14 +47,12 @@ class SignalManager {
|
|
|
51
47
|
if (!this.pingInterval) {
|
|
52
48
|
this.pingInterval = setInterval(() => this.ping(), 3e4);
|
|
53
49
|
}
|
|
54
|
-
const id =
|
|
50
|
+
const id = node_crypto.randomUUID();
|
|
55
51
|
const conn = {
|
|
56
52
|
id,
|
|
57
|
-
user: identity
|
|
53
|
+
user: identity.userEntityRef,
|
|
58
54
|
ws,
|
|
59
|
-
ownershipEntityRefs: identity
|
|
60
|
-
"user:default/guest"
|
|
61
|
-
],
|
|
55
|
+
ownershipEntityRefs: identity.ownershipEntityRefs,
|
|
62
56
|
subscriptions: /* @__PURE__ */ new Set(),
|
|
63
57
|
isAlive: true
|
|
64
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 crypto from 'node:crypto';\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} 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};\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 = `signals-${crypto.randomBytes(8).toString('hex')}`;\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":["crypto","uuid","WebSocket"],"mappings":";;;;;;;;;AAoDO,MAAM,aAAA,CAAc;AAAA,EACjB,WAAA,uBAAiD,GAAA,EAGvD;AAAA,EACM,MAAA;AAAA,EACA,MAAA;AAAA,EACA,YAAA;AAAA,EAER,OAAO,OAAO,OAAA,EAA+B;AAC3C,IAAA,OAAO,IAAI,cAAc,OAAO,CAAA;AAAA,EAClC;AAAA,EAEQ,YAAY,OAAA,EAA+B;AACjD,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AAKtB,IAAA,MAAM,EAAA,GAAK,WAAWA,uBAAA,CAAO,WAAA,CAAY,CAAC,CAAA,CAAE,QAAA,CAAS,KAAK,CAAC,CAAA,CAAA;AAC3D,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA,CAAO,MAAM,EAAE,YAAA,EAAc,IAAI,CAAA;AACvD,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA,gDAAA,CAAkD,CAAA;AAEnE,IAAA,IAAA,CAAK,OAAO,SAAA,CAAU;AAAA,MACpB,EAAA;AAAA,MACA,MAAA,EAAQ,CAAC,SAAS,CAAA;AAAA,MAClB,SAAS,CAAC,MAAA,KACR,IAAA,CAAK,kBAAA,CAAmB,OAAO,YAA6B;AAAA,KAC/D,CAAA;AAED,IAAA,OAAA,CAAQ,SAAA,CAAU,eAAA,CAAgB,MAAM,IAAA,CAAK,YAAY,CAAA;AAAA,EAC3D;AAAA,EAEQ,IAAA,GAAO;AACb,IAAA,IAAA,CAAK,WAAA,CAAY,QAAQ,CAAA,IAAA,KAAQ;AAC/B,MAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACjB,QAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,WAAA,EAAc,IAAA,CAAK,EAAE,CAAA,0BAAA,CAA4B,CAAA;AACnE,QAAA,IAAA,CAAK,GAAG,SAAA,EAAU;AAClB,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,GAAG,IAAA,EAAK;AAAA,IACf,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,UAAA,GAAa;AACnB,IAAA,IAAI,KAAK,YAAA,EAAc;AACrB,MAAA,aAAA,CAAc,KAAK,YAAY,CAAA;AAAA,IACjC;AAIA,IAAA,IAAA,CAAK,WAAA,CAAY,QAAQ,CAAA,IAAA,KAAQ;AAC/B,MAAA,IAAA,CAAK,GAAG,SAAA,EAAU;AAAA,IACpB,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAAA,EACzB;AAAA,EAEA,aAAA,CAAc,IAAe,QAAA,EAA8B;AAEzD,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACtB,MAAA,IAAA,CAAK,eAAe,WAAA,CAAY,MAAM,IAAA,CAAK,IAAA,IAAQ,GAAK,CAAA;AAAA,IAC1D;AAEA,IAAA,MAAM,KAAKC,iBAAA,EAAK;AAChB,IAAA,MAAM,IAAA,GAAO;AAAA,MACX,EAAA;AAAA,MACA,IAAA,EAAM,UAAU,aAAA,IAAiB,oBAAA;AAAA,MACjC,EAAA;AAAA,MACA,mBAAA,EAAqB,UAAU,mBAAA,IAAuB;AAAA,QACpD;AAAA,OACF;AAAA,MACA,aAAA,sBAAmB,GAAA,EAAY;AAAA,MAC/B,OAAA,EAAS;AAAA,KACX;AAEA,IAAA,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,EAAA,EAAI,IAAI,CAAA;AAE7B,IAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,WAAA,EAAc,EAAE,CAAA,UAAA,CAAY,CAAA;AAC9C,IAAA,EAAA,CAAG,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAe;AAC7B,MAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,QACV,CAAA,+BAAA,EAAkC,EAAE,CAAA,EAAA,EAAK,GAAG,CAAA,oBAAA;AAAA,OAC9C;AACA,MAAA,EAAA,CAAG,SAAA,EAAU;AACb,MAAA,IAAA,CAAK,WAAA,CAAY,OAAO,EAAE,CAAA;AAAA,IAC5B,CAAC,CAAA;AAED,IAAA,EAAA,CAAG,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,EAAc,MAAA,KAAmB;AAC/C,MAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,QACV,CAAA,WAAA,EAAc,EAAE,CAAA,kBAAA,EAAqB,IAAI,aAAa,MAAM,CAAA;AAAA,OAC9D;AACA,MAAA,EAAA,CAAG,SAAA,EAAU;AACb,MAAA,IAAA,CAAK,WAAA,CAAY,OAAO,EAAE,CAAA;AAAA,IAC5B,CAAC,CAAA;AAED,IAAA,EAAA,CAAG,EAAA,CAAG,QAAQ,MAAM;AAClB,MAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,MAAA,EAAA,CAAG,IAAA,EAAK;AAAA,IACV,CAAC,CAAA;AAED,IAAA,EAAA,CAAG,EAAA,CAAG,QAAQ,MAAM;AAClB,MAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,IACjB,CAAC,CAAA;AAED,IAAA,EAAA,CAAG,EAAA,CAAG,SAAA,EAAW,CAAC,IAAA,EAAe,QAAA,KAAsB;AACrD,MAAA,IAAA,CAAK,OAAO,KAAA,CAAM,CAAA,iCAAA,EAAoC,EAAE,CAAA,EAAA,EAAK,IAAI,CAAA,CAAE,CAAA;AACnE,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA;AAAA,MACF;AACA,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,UAAU,CAAA;AACvC,QAAA,IAAA,CAAK,aAAA,CAAc,MAAM,IAAI,CAAA;AAAA,MAC/B,SAAS,GAAA,EAAU;AACjB,QAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,UACV,CAAA,yCAAA,EAA4C,EAAE,CAAA,EAAA,EAAK,GAAG,CAAA;AAAA,SACxD;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,aAAA,CAAc,YAA8B,OAAA,EAAqB;AACvE,IAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,WAAA,IAAe,OAAA,CAAQ,OAAA,EAAS;AACrD,MAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,QACV,CAAA,WAAA,EAAc,UAAA,CAAW,EAAE,CAAA,eAAA,EAAkB,QAAQ,OAAO,CAAA;AAAA,OAC9D;AACA,MAAA,UAAA,CAAW,aAAA,CAAc,GAAA,CAAI,OAAA,CAAQ,OAAiB,CAAA;AAAA,IACxD,CAAA,MAAA,IAAW,OAAA,CAAQ,MAAA,KAAW,aAAA,IAAiB,QAAQ,OAAA,EAAS;AAC9D,MAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,QACV,CAAA,WAAA,EAAc,UAAA,CAAW,EAAE,CAAA,mBAAA,EAAsB,QAAQ,OAAO,CAAA;AAAA,OAClE;AACA,MAAA,UAAA,CAAW,aAAA,CAAc,MAAA,CAAO,OAAA,CAAQ,OAAiB,CAAA;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAc,mBAAmB,YAAA,EAA4C;AAC3E,IAAA,IAAI,CAAC,YAAA,CAAa,OAAA,IAAW,CAAC,aAAa,OAAA,EAAS;AAClD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,EAAE,OAAA,EAAS,UAAA,EAAY,OAAA,EAAQ,GAAI,YAAA;AACzC,IAAA,MAAM,cAAc,IAAA,CAAK,SAAA,CAAU,EAAE,OAAA,EAAS,SAAS,CAAA;AACvD,IAAA,IAAI,QAAkB,EAAC;AACvB,IAAA,IAAI,UAAA,CAAW,SAAS,MAAA,EAAQ;AAC9B,MAAA,KAAA,GAAQ,KAAA,CAAM,QAAQ,UAAA,CAAW,SAAS,IACtC,UAAA,CAAW,SAAA,GACX,CAAC,UAAA,CAAW,SAAS,CAAA;AAAA,IAC3B;AAGA,IAAA,IAAA,CAAK,WAAA,CAAY,QAAQ,CAAA,IAAA,KAAQ;AAC/B,MAAA,IAAI,CAAC,IAAA,CAAK,aAAA,CAAc,GAAA,CAAI,OAAO,CAAA,EAAG;AACpC,QAAA;AAAA,MACF;AAGA,MAAA,IACE,UAAA,CAAW,IAAA,KAAS,WAAA,IACpB,CAAC,IAAA,CAAK,mBAAA,CAAoB,IAAA,CAAK,CAAC,GAAA,KAAgB,KAAA,CAAM,QAAA,CAAS,GAAG,CAAC,CAAA,EACnE;AACA,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,IAAA,CAAK,EAAA,CAAG,UAAA,KAAeC,YAAA,CAAU,IAAA,EAAM;AACzC,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,WAAA,EAAa,CAAA,GAAA,KAAO;AAC/B,QAAA,IAAI,GAAA,EAAK;AACP,UAAA,IAAA,CAAK,OAAO,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAK,EAAE,CAAA,EAAA,EAAK,GAAG,CAAA,CAAE,CAAA;AAAA,QAClE;AAAA,MACF,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH;AACF;;"}
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
44
|
-
|
|
45
|
-
socket.
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
113
|
+
if (!subscribedToUpgradeRequests) {
|
|
114
|
+
const server = req.socket?.server;
|
|
115
|
+
if (server) {
|
|
116
|
+
subscribedToUpgradeRequests = true;
|
|
117
|
+
server.on("upgrade", handleUpgrade);
|
|
118
|
+
}
|
|
73
119
|
}
|
|
74
|
-
|
|
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 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 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":"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"}
|
|
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.
|
|
3
|
+
"version": "0.3.20-next.2",
|
|
4
4
|
"backstage": {
|
|
5
5
|
"role": "backend-plugin",
|
|
6
6
|
"pluginId": "signals",
|
|
@@ -40,22 +40,22 @@
|
|
|
40
40
|
"test": "backstage-cli package test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@backstage/backend-plugin-api": "1.
|
|
43
|
+
"@backstage/backend-plugin-api": "1.11.0-next.1",
|
|
44
44
|
"@backstage/config": "1.3.10-next.0",
|
|
45
|
-
"@backstage/plugin-events-node": "0.4.27-next.
|
|
46
|
-
"@backstage/plugin-signals-node": "0.2.6-next.
|
|
45
|
+
"@backstage/plugin-events-node": "0.4.27-next.1",
|
|
46
|
+
"@backstage/plugin-signals-node": "0.2.6-next.1",
|
|
47
47
|
"@backstage/types": "1.2.2",
|
|
48
48
|
"express": "^4.22.0",
|
|
49
49
|
"express-promise-router": "^4.1.0",
|
|
50
50
|
"ws": "^8.20.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@backstage/backend-defaults": "0.18.1-next.
|
|
54
|
-
"@backstage/backend-test-utils": "1.11.8-next.
|
|
53
|
+
"@backstage/backend-defaults": "0.18.1-next.2",
|
|
54
|
+
"@backstage/backend-test-utils": "1.11.8-next.1",
|
|
55
55
|
"@backstage/cli": "0.36.7-next.0",
|
|
56
|
-
"@backstage/plugin-auth-backend": "0.30.2-next.
|
|
57
|
-
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.24-next.
|
|
58
|
-
"@backstage/plugin-events-backend": "0.6.7-next.
|
|
56
|
+
"@backstage/plugin-auth-backend": "0.30.2-next.1",
|
|
57
|
+
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.24-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",
|