Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
@cap-js-community/websocket
Advanced tools
Exposes a WebSocket protocol via WebSocket standard or Socket.IO for CDS services. Runs in context of the SAP Cloud Application Programming Model (CAP) using @sap/cds (CDS Node.js).
npm add @cap-js-community/websocket
in @sap/cds
project@ws
@websocket
@protocol: 'ws'
@protocol: 'websocket'
@protocol: [{ kind: 'websocket', path: 'chat' }]
@protocol: [{ kind: 'ws', path: 'chat' }]
cds-serve
to start servernpm add @cap-js-community/websocket
in @sap/cds
project@protocol: 'websocket'
service ChatService {
function message(text: String) returns String;
event received {
text: String;
}
}
module.exports = (srv) => {
srv.on("message", async (req) => {
await srv.emit("received", req.data);
return req.data.text;
});
};
In browser environment implement the websocket client: index.html
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
const socket = new WebSocket(protocol + window.location.host + "/ws/chat");
socket.send(
JSON.stringify({
event: "message",
data: { text: input.value },
}),
);
socket.addEventListener("message", (message) => {
const payload = JSON.parse(message.data);
switch (payload.event) {
case "received":
console.log(payload.data.text);
break;
}
});
kind: socket.io
)const socket = io("/chat", { path: "/ws" });
socket.emit("message", { text: "Hello World" });
socket.on("received", (message) => {
console.log(message.text);
});
The CDS Websocket module supports the following use-cases:
The CDS websocket server is exposed on cds
object implementation-independent at cds.ws
and implementation-specific at
cds.wss
for WebSocket Standard or cds.io
for Socket.IO. Additional listeners can be registered bypassing CDS definitions and runtime.
WebSocket server options can be provided via cds.websocket.options
.
Default protocol path is /ws
and can be overwritten via cds.env.protocols.websocket.path
resp. cds.env.protocols.ws.path
;
The CDS websocket server supports the following two websocket implementations:
cds.websocket.kind: "ws"
(default)cds.websocket.kind: "socket.io"
cds.websocket.impl
(e.g. cds.websocket.impl: './server/xyz.js'
).The server implementation abstracts from the concrete websocket implementation. The websocket client still needs to be implemented websocket implementation specific.
Annotated services with websocket protocol are exposed at endpoint: /ws/<service-path>
:
Websocket client connection happens as follows for exposed endpoints:
const socket = new WebSocket("ws://localhost:4004/ws/chat");
const socket = io("/chat", { path: "/ws" })
Websocket services can contain events that are exposed as websocket events. Emitting an event on the service, broadcasts the event to all websocket clients.
@protocol: 'ws'
@path: 'chat'
service ChatService {
event received {
text: String;
}
}
In addition, also non-websocket services can contain events that are exposed as websocket events:
@protocol: 'odata'
@path: 'chat'
service ChatService {
entity Chat as projection on chat.Chat;
function message(text: String) returns String;
@websocket
event received {
text: String;
}
}
Although the service is exposed as an OData protocol at /odata/v4/chat
, the service events annotated with @websocket
or
@ws
are exposed as websocket events under the websocket protocol path as follows: /ws/chat
. Entities and operations
are not exposed, as the service itself is not marked as websocket protocol.
Non-websocket service events are only active when at least one websocket enabled service is available (i.e. websocket protocol adapter is active).
Each CDS handler request context is extended to hold the current server socket
instance of the event.
It can be accessed via the service websocket facade via req.context.ws.service
or cds.context.ws.service
.
In addition the native websocket server socket can be accessed via req.context.ws.socket
or cds.context.ws.socket
.
Events can be directly emitted via the native socket
, bypassing CDS runtime, if necessary.
The service facade provides native access to websocket implementation independent of CDS context
and is accessible on socket via socket.facade
or in CDS context via req.context.ws.service
.:
It abstracts from the concrete websocket implementation by exposing the following public interface:
service: Object
: Service definitionpath: String
: Service pathsocket: Object
: Server socketcontext: Object
: CDS context object for the websocket server socketon(event: String, callback: Function)
: Register websocket eventasync emit(event: String, data: Object)
: Emit websocket event with dataasync broadcast(event: String, data: Object, user: {include: String[], exclude: String[]}?, contexts: String[]?, identifier: {include: String[], exclude: String[]}?)
: Broadcast websocket event (except to sender) by optionally restrict to users, contexts or identifiersasync broadcastAll(event: String, data: Object, user: {include: String[], exclude: String[]}?, contexts: String[]?, identifier: {include: String[], exclude: String[]}?)
: Broadcast websocket event (including to sender) by optionally restrict to users, contexts or identifiersasync enter(context: String)
: Enter a contextasync exit(context: String)
: Exit a contextasync disconnect()
: Disconnect server socketonDisconnect(callback: Function)
: Register callback function called on disconnect of server socketFor each server websocket connection the standard CDS middlewares are applied. That means, that especially the correct CDS context is set up and the configured authorization strategy is applied.
WebSockets are processed tenant aware. Especially for broadcasting events tenant isolation is ensured, that only websocket clients connected for the same tenant are notified in tenant context. Tenant isolation is also ensured over remote distribution via Redis.
Authentication only works via AppRouter (e.g. using a UAA configuration), as the auth token is forwarded
via authorization header bearer token by AppRouter to backend instance. CDS middlewares process the auth token and
set the auth info accordingly. Authorization scopes are checked as defined in the CDS services @requires
annotations and
authorization restrictions are checked as defined in the CDS services @restrict
annotations.
In most situations only websocket events shall be broadcast, in case the primary transaction succeeded.
It can be done manually, by emitting CDS event as part of the req.on("succeeded")
handler.
req.on("succeeded", async () => {
await srv.emit("received", req.data);
});
Alternatively you can leverage the CAP in-memory outbox via cds.outboxed
as follows:
const chatService = cds.outboxed(await cds.connect.to("ChatService"));
await chatService.emit("received", req.data);
This has the benefit, that the event emitting is coupled to the success of the primary transaction. Still the asynchronous event processing could fail, and would not be retried anymore. That's where the CDS persistent outbox comes into play.
Websocket events can also be sent via the CDS persistent outbox. That means, the CDS events triggering the websocket broadcast are added to the CDS persistent outbox when the primary transaction succeeded. The events are processed asynchronously and transactional safe in a separate transaction. It is ensured, that the event is processed in any case, as outbox keeps the outbox entry open, until the event processing succeeded.
The transactional safety can be achieved using cds.outboxed
with kind persistent-outbox
as follows:
const chatService = cds.outboxed(await cds.connect.to("ChatService"), {
kind: "persistent-outbox",
});
await chatService.emit("received", req.data);
In that case, the websocket event is broadcast to websocket clients exactly once, when the primary transaction succeeds. In case of execution errors, the event broadcast is retried automatically, while processing the persistent outbox.
Events are broadcast to all websocket clients, including clients established in context of current event context user.
To influence event broadcasting based on current context user, the annotation @websocket.user
or @ws.user
is available on
event level and event element level (alternatives include @websocket.broadcast.user
or @ws.broadcast.user
):
Valid annotation values are:
'includeCurrent'
: Current event context user is statically included everytime during broadcasting to websocket clients.
Only websocket clients established in context to that user are respected during event broadcast.'excludeCurrent'
: Current event context user is statically excluded everytime during broadcasting to websocket clients.
All websocket clients established in context to that user are not respected during event broadcast.'includeCurrent'
: Current event context user is dynamically included during broadcasting to websocket clients,
based on the value of the annotated event element. If truthy, only websocket clients established in context to that user are respected during event broadcast.'excludeCurrent'
: Current event context user is dynamically excluded during broadcasting to websocket clients,
based on the value of the annotated event element. If truthy, all websocket clients established in context to that user are not respected during event broadcast.Furthermore, also additional equivalent annotations alternatives are available:
Include current user:
@websocket.currentUser.include
@ws.currentUser.include
@websocket.broadcast.currentUser.include
@ws.broadcast.currentUser.include
Exclude current user:
@websocket.currentUser.exclude
@ws.currentUser.exclude
@websocket.broadcast.currentUser.exclude
@ws.broadcast.currentUser.exclude
It is possible to broadcast events to a subset of clients. By entering or exiting contexts, the server can be instructed to
determined based on the event, to which subset of clients the event shall be emitted. To specify which data parts of the
event are leveraged for setting up the context, the annotation @websocket.context
or @ws.context
is available on
event element level (alternatives include @websocket.broadcast.context
or @ws.broadcast.context
). For static contexts
the annotation can also be used on event level, providing a static event context string.
event received {
@websocket.context
ID: UUID;
text: String;
}
This sets up the event context based on the unique ID of the event data.
The annotation can be used on multiple event elements setting up different event contexts in parallel, if event shall be broadcast/emitted into multiple contexts at the same time.
event received {
@websocket.context
ID: UUID;
@websocket.context
name: String;
text: String;
}
Event contexts can also be established via event elements of many
or array of
type:
event received {
@websocket.context
ID: many UUID;
text: String;
}
This allows setting up an unspecified number of different event contexts in parallel during runtime.
Event contexts support all CDS/JS types. The serialization is performed as follows:
Date
: context.toISOString()
Object
: JSON.stringify(context)
String(context)
To manage event contexts the following options exist:
req
exposes the websocket facade via req.context.ws.service
providing the following context functions
enter(context: String)
- Enter the current server socket into the passed contextexit(context: String)
- Exit the current server socket from the passed contextwsContext
event from client socket
socket.send(JSON.stringify({ event: "wsContext", data: { context: "..." } }));
socket.emit("wsContext", { context: "..." });
socket.send(JSON.stringify({ event: "wsContext", data: { context: "...", exit: true } }));
socket.emit("wsContext", { context: "...", exit: true });
Multiple contexts can be entered for the same server socket at the same time. Furthermore, a service operation named
wsContext
is invoked, if existing on the websocket enabled CDS service. Event context isolation is also ensured
over remote distribution via Redis.
For Socket.IO (kind: socket.io
) contexts are implemented leveraging Socket.IO rooms.
Events are broadcast to all websocket clients, including clients that performed certain action. When events are send as part of websocket context, access to current socket is given, but if actions are performed outside websocket context, there are no means to identify the client that performed the action.
That's where the event client identifier come into play. Client identifier are unique consumer-provided strings, that are provided during the websocket connection to identify the websocket client as well as in other request cases (e.g. OData call). When an OData call with a client identifier is performed, it can be used to restrict the websocket event broadcasting.
In some cases, the websocket clients shall be restricted on an instance basis. There are use-cases, that only certain
clients are informed about an event and also in other cases the client shall not be informed about the event, that was triggered by the same client (maybe via a different channel, e.g. OData).
Therefore, websocket clients can be identified optionally by a unique identifier provided as URL parameter option ?id=<globally unique value>
.
The annotation @websocket.identifier.include
or @ws.identifier.include
is available on event level and event element level
to influence event broadcasting based websocket client identifier to include certain clients based on their identifier (not listed clients are no longer respected when set)
(alternatives include @websocket.broadcast.identifier.include
or @ws.broadcast.identifier.include
):
The annotation @websocket.identifier.exclude
or @ws.identifier.exclude
is available on event level and event element level
to influence event broadcasting based websocket client identifier to exclude certain clients based on their identifier
(alternatives include @websocket.broadcast.identifier.exclude
or @ws.broadcast.identifier.exclude
):
Valid annotation values are:
The unique identifier can be provided for a websocket client as follows:
socket = new WebSocket("ws://localhost:4004/ws/chat?id=1234");
const socket = io("/chat?id=1234", { path: "/ws" });
The websocket implementation allows to provide event emit headers to dynamically control websocket processing. The following headers are available:
wsCurrentUser.include: boolean
currentUser.include: boolean
wsIncludeCurrentUser: boolean
includeCurrentUser: boolean
wsCurrentUser.exclude: boolean
currentUser.exclude: boolean
wsExcludeCurrentUser: boolean
excludeCurrentUser: boolean
wsContexts: String[] | String
contexts: String[] | String
wsContext: String[] | String
context: String[] | String
wsIdentifier.include: String[] | String
identifier.include: String[] | String
wsIdentifierInclude: String[] | String
identifierInclude: String[] | String
wsIdentifier.exclude: String[] | String
identifier.exclude: String[] | String
wsIdentifierExclude: String[] | String
identifierExclude: String[] | String
Emitting events with headers can be performed as follows:
await srv.emit("customEvent", { ... }, {
contexts: ["..."],
currentUser: {
exclude: req.data.type === "1"
},
identifier: {
include: ["..."],
exclude: "...",
},
});
The respective event annotations (described in sections above) are respected in addition to event emit header specification, so that primitive typed values have priority when specified as part of headers and array-like data is unified.
Per default the CDS websocket format is json
, as CDS internally works with JSON objects.
WS Standard and Socket.IO support JSON format as follows:
{ event, data }
CDS WebSocket module supports the SAP Push Channel Protocol (PCP) out-of-the-box.
A PCP message has the following structure:
pcp-action:MESSAGE
pcp-body-type:text
field1:value1
field2:value2
this is the body!
To configure the PCP format, the service needs to be annotated in addition with @websocket.format: 'pcp'
or @ws.format: 'pcp'
:
@ws
@ws.format: 'pcp'
service PCPService {
// ...
}
With this configuration WebSocket events consume or produce PCP formatted messages. To configure the PCP message format the following annotations are available:
@ws.pcp.action
: Correlate pcp-action
in PCP message to identify the CDS operation via annotation. If not defined, the operation name is correlated.@ws.pcp.message
: Correlate the PCP message body to the operation parameter representing the message.@ws.pcp.event
: Expose the CDS event as pcp-event
field in the PCP message.@ws.pcp.message
: Expose a static message text as PCP message body.@ws.pcp.action
: Exposes a static action as pcp-action
field in the PCP message. Default MESSAGE
.@ws.pcp.message
: Expose the string value of the annotated event element as PCP message body.@ws.pcp.action
: Expose the string value of the annotated event element as pcp-action
field in the PCP message. Default MESSAGE
.A custom websocket format implementation can be provided via a path relative to the project root
in @websocket.format
resp. @ws.format
annotation (e.g. @ws.format: './format/xyz.js'
).
The custom format class needs to implement the following functions:
{ event, data }
socket.io
, it can also be a JSON object.In addition, it can implement the following functions (optional):
Every time a server socket is connected via websocket client, the CDS service is notified by calling the corresponding service operation:
Connect
: Invoke service operation wsConnect
, if availableDisconnect
: Invoke service operation wsDisconnect
, if availableAuthorization in provided in production by approuter component (e.g. via XSUAA auth). Valid UAA bindings for approuter and backend are necessary, so that the authorization flow is working. Locally, the following default environment files need to exist:
test/_env/default-env.json
{
"VCAP_SERVICES": {
"xsuaa": [{}]
}
}
test/_env/approuter/default-services.json
{
"uaa": {}
}
Approuter is configured to support websockets in xs-app.json
according to @sap/approuter - websockets property:
{
"websockets": {
"enabled": true
}
}
For local testing a mocked basic authorization is hardcoded in flp.html/index.html
.
Operations comprise actions and function in the CDS service that are exposed by CDS service either unbound (static level) or bound (entity instance level). Operations are exposed as part of the websocket protocol as described below.
Operation results will be provided via optional websocket acknowledgement callback.
Operation results are only supported with Socket.IO (
kind: socket.io
) using acknowledgement callbacks.
Each unbound function and action is exposed as websocket event. The signature (parameters and return type) is passed through without additional modification. Operation result will be provided as part of acknowledgment callback.
The websocket adapter tries to call the following special operations on the CDS service, if available:
wsConnect
: Callback to notify that a socket was connectedwsDisconnect
: Callback to notify that a socket was disconnectedwsContext
: Callback to notify that a socket changed the event context (details see section Event Contexts)Each service entity is exposed as CRUD interface via as special events as proposed here.
The event is prefixed with the entity name and has the CRUD operation as suffix, e.g. Books:create
.
In addition, also bound functions and actions are included into these schema, e.g. Books:sell
.
The signature (parameters and return type) is passed through without additional modification.
It is expected, that the event payload contains the primary key information.
CRUD/action/function result will be provided as part of acknowledgment callback.
Create, Read, Update and Delete (CRUD) actions are mapped to websocket events as follows:
<entity>:create
: Create an entity instance<entity>:read
: Read an entity instance by key<entity>:readDeep
: Read an entity instance deep (incl. deep compositions) by key<entity>:update
: Update an entity instance by key<entity>:delete
: Delete an entity instance by key<entity>:list
: List all entity instances<entity>:<operation>
: Call a bound entity operation (action/function)Events can be emitted and the response can be retrieved via acknowledgment callback (kind: socket.io
only).
CRUD events that modify entities automatically emit another event after successful processing:
<entity>:create => <entity>:created
: Entity instance has been updated<entity>:update => <entity>:updated
: Entity instance has been created<entity>:delete => <entity>:deleted
: Entity instance has been deletedBecause of security concerns, it can be controlled which data of those events is broadcast,
via annotations @websocket.broadcast
or @ws.broadcast
on entity level.
@websocket.broadcast = 'key'
@websocket.broadcast.content = 'key'
@ws.broadcast = 'key'
@ws.broadcast.content = 'key'
@websocket.broadcast = 'data'
@websocket.broadcast.content = 'data'
@ws.broadcast = 'data'
@ws.broadcast.content = 'data'
@websocket.broadcast = 'none'
@websocket.broadcast.content = 'none'
@ws.broadcast = 'none'
@ws.broadcast.content = 'none'
If the CRUD broadcast event is modeled as part of CDS service the annotations above are ignored for that event,
and the broadcast data is filtered along the event elements. As character :
is not allowed in CDS service event names,
character :
is replaced by a scoped event name using character .
.
Example:
WebSocket Event: Object:created
is mapped to CDS Service Event: Object.created
Per default, events are broadcast to every connected socket, expect the socket, that was called with the CRUD event.
To also include the triggering socket within the broadcast, this can be controlled via annotations
@websocket.broadcast.all
or @ws.broadcast.all
on entity level.
The example UI5 todo
application using Socket.IO can be found at test/_env/app/todo
.
Example application can be started by:
npm start
npm run start:approuter
npm run start:uaa
An example chat
application using Socket.IO can be found at test/_env/app/chat
.
Example application can be started by:
npm start
npm run start:socketio
npm run start:approuter
npm run start:uaa
npm run start:socketio:uaa
Unit-test can be found in folder test
and can be executed via npm test
;
The basic unit-test setup for WebSockets in CDS context looks as follows:
"use strict";
const cds = require("@sap/cds");
const WebSocket = require("ws");
cds.test(__dirname + "/..");
const authorization = `Basic ${Buffer.from("alice:alice").toString("base64")}`;
describe("WebSocket", () => {
let socket;
beforeAll((done) => {
const port = cds.app.server.address().port;
socket = new WebSocket(`ws://localhost:${port}/ws/chat`, {
headers: {
authorization,
},
});
});
afterAll(() => {
cds.ws.close();
socket.close();
});
test("Test", (done) => {
socket.send(
JSON.stringify({
event: "event",
data: {},
}),
);
});
});
"use strict";
const cds = require("@sap/cds");
const ioc = require("socket.io-client");
cds.test(__dirname + "/..");
cds.env.websocket = {
kind: "socket.io",
impl: null,
};
const authorization = `Basic ${Buffer.from("alice:alice").toString("base64")}`;
describe("WebSocket", () => {
let socket;
beforeAll((done) => {
const port = cds.app.server.address().port;
socket = ioc(`http://localhost:${port}/chat`, {
path: "/ws",
extraHeaders: {
authorization,
},
});
socket.on("connect", done);
});
afterAll(() => {
cds.ws.close();
socket.disconnect();
});
test("Test", (done) => {
socket.emit("event", {}, (result) => {
expect(result).toBeDefined();
done();
});
});
});
An Adapter is a server-side component which is responsible for broadcasting events to all or a subset of clients.
Every event that is sent to multiple clients is sent to all matching clients connected to the current server and published in a Redis channel, and received by the other websocket servers of the cluster. The app needs to be bound to a Redis service instance to set up and connect Redis client.
The following adapters for WS Standard are supported out-of-the-box.
To use the Redis Adapter (basic publish/subscribe), the following steps have to be performed:
cds.websocket.adapter.impl: "redis"
cds.websocket.adapter.active: false
to disable Redis adaptercds.websocket.adapter.active: true
to enable Redis adaptercds.websocket.adapter.local: true
to enable Redis adapterdefault-env.json
need to exist with Redis configurationcds.websocket.adapter.options
cds.websocket.adapter.options.key
. Default value is websocket
.cds.websocket.adapter.config
A custom websocket adapter implementation can be provided via a path relative to the project root
with the configuration cds.websocket.adapter.impl
(e.g. cds.websocket.adapter.impl: './adapter/xyz.js'
).
The custom adapter class needs to implement the following functions:
In addition, it can implement the following functions (optional):
The following adapters for Socket.IO are supported out-of-the-box.
To use the Redis Adapter, the following steps have to be performed:
npm install @socket.io/index-adapter
cds.websocket.adapter.impl: "@socket.io/index-adapter"
default-env.json
file need to exist with index configurationcds.websocket.adapter.options
cds.websocket.adapter.options.key
. Default value is socket.io
.Details: https://socket.io/docs/v4/index-adapter/
To use the Redis Stream Adapter, the following steps have to be performed:
npm install @socket.io/index-streams-adapter
cds.websocket.adapter.impl: "@socket.io/index-streams-adapter"
default-env.json
file need to exist with index configurationcds.websocket.adapter.options
cds.websocket.adapter.options.streamName
. Default value is socket.io
.Details: https://socket.io/docs/v4/index-streams-adapter/
A custom websocket adapter implementation can be provided via a path relative to the project root
with the configuration cds.websocket.adapter.impl
(e.g. cds.websocket.adapter.impl: './adapter/xyz.js'
).
The custom adapter need to fulfill the Socket.IO adapter interface (https://socket.io/docs/v4/adapter/).
This module also works on a deployed infrastructure like Cloud Foundry (CF) or Kubernetes (K8s).
An example Cloud Foundry deployment can be found in test/_env
:
cd test/_env
npm run cf:push
approuter
and backend
in test/_env
and pushes to Cloud Foundry
In deployed infrastructure, websocket protocol is exposed via Web Socket Secure (WSS) at wss://
over an encrypted TLS connection.
For WebSocket standard the following setup in browser environment is recommended to cover deployed and local use-case:
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
const socket = new WebSocket(protocol + window.location.host + "/ws/chat");
This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.
Copyright 2023 SAP SE or an SAP affiliate company and websocket contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.
Version 1.2.0 - 2024-09-04
FAQs
WebSocket adapter for CDS
The npm package @cap-js-community/websocket receives a total of 108 weekly downloads. As such, @cap-js-community/websocket popularity was classified as not popular.
We found that @cap-js-community/websocket demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.