From 0bce422e30f65c7ba45b9324bd55aa0b9272b15b Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 10 Feb 2022 14:09:23 +0100 Subject: [PATCH] Add API documentation. Taken from commit deb2494193a51321273070b39739e625651d40ea of https://github.com/nextcloud/spreed/ Should be kept at the signaling server repository as the API is defined and implemented here. --- docs/standalone-signaling-api-v1.md | 802 ++++++++++++++++++++++++++++ 1 file changed, 802 insertions(+) create mode 100644 docs/standalone-signaling-api-v1.md diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md new file mode 100644 index 0000000..f98e841 --- /dev/null +++ b/docs/standalone-signaling-api-v1.md @@ -0,0 +1,802 @@ +# External signaling API + +This document gives a rough overview on the API version 1.0 of the Spreed +signaling server. Clients can use the signaling server to send realtime +messages between different users / sessions. + +The API describes the various messages that can be sent by a client or the +server to join rooms or distribute events between clients. + +Depending on the server implementation, clients can use WebSockets (preferred) +or COMET (i.e. long-polling) requests to communicate with the signaling server. + +For WebSockets, only the API described in this document is necessary. For COMET, +an extension to this API is required to identify a (virtual) connection between +multiple requests. The payload for COMET is the messages as described below. + +See [Internal signaling API](internal-signaling.md) for the API of the regular PHP backend. + + +## Request + + { + "id": "unique-request-id", + "type": "the-request-type", + "the-request-type": { + ...object defining the request... + } + } + +Example: + + { + "id": "123-abc", + "type": "samplemessage", + "samplemessage": { + "foo": "bar", + "baz": 1234 + } + } + + +## Response + + { + "id": "unique-request-id-from-request-if-present", + "type": "the-response-type", + "the-response-type": { + ...object defining the response... + } + } + +Example: + + { + "id": "123-abc", + "type": "sampleresponse", + "sampleresponse": { + "hello": "world!" + } + } + + +## Errors + +The server can send error messages as a response to any request the client has +sent. + +Message format: + + { + "id": "unique-request-id-from-request-if-present", + "type": "error", + "error": { + "code": "the-internal-message-id", + "message": "human-readable-error-message", + "details": { + ...optional additional details... + } + } + } + + +## Backend requests + +For some messages, the signaling server has to perform a request to the +Nextcloud backend (e.g. to validate the user authentication). The backend +must be able to verify the request to make sure it is coming from a valid +signaling server. + +Also the Nextcloud backend can send requests to the signaling server to notify +about events related to a room or user (e.g. a user is no longer invited to +a room). Here the signaling server must be able to verify the request to check +if it is coming from a valid Nextcloud instance. + +Therefore all backend requests, either from the signaling server or vice versa +must contain two additional HTTP headers: + +- `Spreed-Signaling-Random`: Random string of at least 32 bytes. +- `Spreed-Signaling-Checksum`: SHA256-HMAC of the random string and the request + body, calculated with a shared secret. The shared secret is configured on + both sides, so the checksum can be verified. +- `Spreed-Signaling-Backend`: Base URL of the Nextcloud server performing the + request. + +### Example + +- Request body: `{"type":"auth","auth":{"version":"1.0","params":{"hello":"world"}}}` +- Random: `afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6` +- Shared secret: `MySecretValue` +- Calculated checksum: `3c4a69ff328299803ac2879614b707c807b4758cf19450755c60656cac46e3bc` + + +## Establish connection + +This must be the first request by a newly connected client and is used to +authenticate the connection. No other messages can be sent without a successful +`hello` handshake. + +Message format (Client -> Server): + + { + "id": "unique-request-id", + "type": "hello", + "hello": { + "version": "the-protocol-version-must-be-1.0", + "auth": { + "url": "the-url-to-the-auth-backend", + "params": { + ...object containing auth params... + } + } + } + } + +Message format (Server -> Client): + + { + "id": "unique-request-id-from-request", + "type": "hello", + "hello": { + "sessionid": "the-unique-session-id", + "resumeid": "the-unique-resume-id", + "userid": "the-user-id-for-known-users", + "version": "the-protocol-version-must-be-1.0", + "server": { + "features": ["optional", "list, "of", "feature", "ids"], + ...additional information about the server... + } + } + } + + +### Backend validation + +The server validates the connection request against the passed auth backend +(needs to make sure the passed url / hostname is in a whitelist). It performs +a POST request and passes the provided `params` as JSON payload in the body +of the request. + +Message format (Server -> Auth backend): + + { + "type": "auth", + "auth": { + "version": "the-protocol-version-must-be-1.0", + "params": { + ...object containing auth params from hello request... + } + } + } + +If the auth params are valid, the backend returns information about the user +that is connecting (as JSON response). + +Message format (Auth backend -> Server): + + { + "type": "auth", + "auth": { + "version": "the-protocol-version-must-be-1.0", + "userid": "the-user-id-for-known-users", + "user": { + ...additional data of the user... + } + } + } + +Anonymous connections that are not mapped to a user in Nextcloud will have an +empty or omitted `userid` field in the response. If the connection can not be +authorized, the backend returns an error and the hello request will be rejected. + + +### Error codes + +- `unsupported-version`: The requested version is not supported. +- `auth-failed`: The session could not be authenticated. +- `too-many-sessions`: Too many sessions exist for this user id. +- `invalid_backend`: The requested backend URL is not supported. +- `invalid_client_type`: The [client type](#client-types) is not supported. +- `invalid_token`: The passed token is invalid (can happen for + [client type `internal`](#client-type-internal)). + + +### Client types + +In order to support clients with different functionality on the server, an +optional `type` can be specified in the `auth` struct when connecting to the +server. If no `type` is present, the default value `client` will be used and +a regular "user" client is created internally. + +Message format (Client -> Server): + + { + "id": "unique-request-id", + "type": "hello", + "hello": { + "version": "the-protocol-version-must-be-1.0", + "auth": { + "type": "the-client-type", + ...other attributes depending on the client type... + "params": { + ...object containing auth params... + } + } + } + } + +The key `params` is required for all client types, other keys depend on the +`type` value. + + +#### Client type `client` (default) + +For the client type `client` (which is the default if no `type` is given), the +URL to the backend server for this client must be given as described above. + +This client type must be supported by all server implementations of the +signaling protocol. + + +#### Client type `internal` + +"Internal" clients are used for connections from internal services where the +connection doesn't map to a user (or session) in Nextcloud. + +These clients can skip some internal validations, e.g. they can join any room, +even if they have not been invited (which is not possible as the client doesn't +map to a user). This client type is not required to be supported by server +implementations of the signaling protocol, but some additional services might +not work without "internal" clients. + +To authenticate the connection, the `params` struct must contain keys `random` +(containing any random string of at least 32 bytes) and `token` containing the +SHA-256 HMAC of `random` with a secret that is shared between the signaling +server and the service connecting to it. + + +## Resuming sessions + +If a connection was interrupted for a client, the server may decide to keep the +session alive for a short time, so the client can reconnect and resume the +session. + +In this case, no complete `hello` handshake is required and a client can use +a shorter `hello` request. On success, the session will resume as if no +interruption happened, i.e. the client will stay in his room and will get all +messages from the time the interruption happened. + +Message format (Client -> Server): + + { + "id": "unique-request-id", + "type": "hello", + "hello": { + "version": "the-protocol-version-must-be-1.0", + "resumeid": "the-resume-id-from-the-original-hello-response" + } + } + +Message format (Server -> Client): + + { + "id": "unique-request-id-from-request", + "type": "hello", + "hello": { + "sessionid": "the-unique-session-id", + "version": "the-protocol-version-must-be-1.0" + } + } + +If the session is no longer valid (e.g. because the resume was too late), the +server will return an error and a normal `hello` handshake has to be performed. + + +### Error codes + +- `no_such_session`: The session id is no longer valid. + + +## Releasing sessions + +By default, the signaling server tries to maintain the session so clients can +resume it in case of intermittent connection problems. + +To support cases where a client wants to close the connection and release all +session data, he can send a `bye` message so the server knows he doesn't need +to keep data for resuming. + +Message format (Client -> Server): + + { + "id": "unique-request-id", + "type": "bye", + "bye": {} + } + +Message format (Server -> Client): + + { + "id": "unique-request-id-from-request", + "type": "bye", + "bye": {} + } + +After the `bye` has been confirmed, the session can no longer be used. + + +## Join room + +After joining the room through the PHP backend, the room must be changed on the +signaling server, too. + +Message format (Client -> Server): + + { + "id": "unique-request-id", + "type": "room", + "room": { + "roomid": "the-room-id", + "sessionid": "the-nextcloud-session-id" + } + } + +- The client can ask about joining a room using this request. +- The session id received from the PHP backend must be passed as `sessionid`. +- The `roomid` can be empty to leave the room. +- A session can only be connected to one room, i.e. joining a room will leave + the room currently in. + +Message format (Server -> Client): + + { + "id": "unique-request-id-from-request", + "type": "room", + "room": { + "roomid": "the-room-id", + "properties": { + ...additional room properties... + } + } + } + +- Sent to confirm a request from the client. +- The `roomid` will be empty if the client is no longer in a room. +- Can be sent without a request if the server moves a client to a room / out of + the current room or the properties of a room change. + + +### Backend validation + +Rooms are managed by the Nextcloud backend, so the signaling server has to +verify that a room exists and a user is allowed to join it. + +Message format (Server -> Room backend): + + { + "type": "room", + "room": { + "version": "the-protocol-version-must-be-1.0", + "roomid": "the-room-id", + "userid": "the-user-id-for-known-users", + "sessionid": "the-nextcloud-session-id", + "action": "join-or-leave" + } + } + +The `userid` is empty or omitted for anonymous sessions that don't belong to a +user in Nextcloud. + +Message format (Room backend -> Server): + + { + "type": "room", + "room": { + "version": "the-protocol-version-must-be-1.0", + "roomid": "the-room-id", + "properties": { + ...additional room properties... + } + } + } + +If the room does not exist or can not be joined by the given (or anonymous) +user, the backend returns an error and the room request will be rejected. + + +### Error codes + +- `no_such_room`: The requested room does not exist or the user is not invited + to the room. + + +## Leave room + +To leave a room, a [join room](#join-room) message must be sent with an empty +`roomid` parameter. + + +## Room events + +When users join or leave a room, the server generates events that are sent to +all sessions in that room. Such events are also sent to users joining a room +as initial list of users in the room. Multiple user joins/leaves can be batched +into one event to reduce the message overhead. + +Message format (Server -> Client, user(s) joined): + + { + "type": "event" + "event": { + "target": "room", + "type": "join", + "join": [ + ...list of session objects that joined the room... + ] + } + } + +Room event session object: + + { + "sessionid": "the-unique-session-id", + "userid": "the-user-id-for-known-users", + "user": { + ...additional data of the user as received from the auth backend... + } + } + +Message format (Server -> Client, user(s) left): + + { + "type": "event" + "event": { + "target": "room", + "type": "leave", + "leave": [ + ...list of session ids that left the room... + ] + } + } + +Message format (Server -> Client, user(s) changed): + + { + "type": "event" + "event": { + "target": "room", + "type": "change", + "change": [ + ...list of sessions that have changed... + ] + } + } + + +## Room list events + +When users are invited to rooms or are disinvited from them, they get notified +so they can update the list of available rooms. + +Message format (Server -> Client, invited to room): + + { + "type": "event" + "event": { + "target": "roomlist", + "type": "invite", + "invite": [ + "roomid": "the-room-id", + "properties": [ + ...additional room properties... + ] + ] + } + } + +Message format (Server -> Client, disinvited from room): + + { + "type": "event" + "event": { + "target": "roomlist", + "type": "disinvite", + "disinvite": [ + "roomid": "the-room-id" + ] + } + } + + +Message format (Server -> Client, room updated): + + { + "type": "event" + "event": { + "target": "roomlist", + "type": "update", + "update": [ + "roomid": "the-room-id", + "properties": [ + ...additional room properties... + ] + ] + } + } + + +## Participants list events + +When the list of participants or flags of a participant in a room changes, an +event is triggered by the server so clients can update their UI accordingly or +trigger actions like starting calls with other peers. + +Message format (Server -> Client, participants change): + + { + "type": "event" + "event": { + "target": "participants", + "type": "update", + "update": [ + "roomid": "the-room-id", + "users": [ + ...list of changed participant objects... + ] + ] + } + } + +If a participant has the `inCall` flag set, he has joined the call of the room +and a WebRTC peerconnection should be established if the local client is also +in the call. In that case the participant information will contain properties +for both the signaling session id (`sessionId`) and the Nextcloud session id +(`nextcloudSessionId`). + + +## Room messages + +The server can notify clients about events that happened in a room. Currently +such messages are only sent out when chat messages are posted to notify clients +they should load the new messages. + +Message format (Server -> Client, chat messages available): + + { + "type": "event" + "event": { + "target": "room", + "type": "message", + "message": { + "roomid": "the-room-id", + "data": { + "type": "chat", + "chat": { + "refresh": true + } + } + } + } + } + + +## Sending messages between clients + +Messages between clients are sent realtime and not stored by the server, i.e. +they are only delivered if the recipient is currently connected. This also +applies to rooms, where only sessions currently in the room will receive the +messages, but not if they join at a later time. + +Use this for establishing WebRTC connections between peers, i.e. sending offers, +answers and candidates. + +Message format (Client -> Server, to other sessions): + + { + "id": "unique-request-id", + "type": "message", + "message": { + "recipient": { + "type": "session", + "sessionid": "the-session-id-to-send-to" + }, + "data": { + ...object containing the data to send... + } + } + } + +Message format (Client -> Server, to all sessions of a user): + + { + "id": "unique-request-id", + "type": "message", + "message": { + "recipient": { + "type": "user", + "userid": "the-user-id-to-send-to" + }, + "data": { + ...object containing the data to send... + } + } + } + +Message format (Client -> Server, to all sessions in the same room): + + { + "id": "unique-request-id", + "type": "message", + "message": { + "recipient": { + "type": "room" + }, + "data": { + ...object containing the data to send... + } + } + } + +Message format (Server -> Client, receive message) + + { + "type": "message", + "message": { + "sender": { + "type": "the-type-when-sending", + "sessionid": "the-session-id-of-the-sender", + "userid": "the-user-id-of-the-sender" + }, + "data": { + ...object containing the data of the message... + } + } + } + +- The `userid` is omitted if a message was sent by an anonymous user. + + +# Internal signaling server API + +The signaling server provides an internal API that can be called from Nextcloud +to trigger events from the server side. + + +## Rooms API + +The base URL for the rooms API is `/api/vi/room/`, all requests must be +sent as `POST` request with proper checksum headers as described above. + + +### New users invited to room + +This can be used to notify users that they are now invited to a room. + +Message format (Backend -> Server) + + { + "type": "invite" + "invite" { + "userids": [ + ...list of user ids that are now invited to the room... + ], + "alluserids": [ + ...list of all user ids that invited to the room... + ], + "properties": [ + ...additional room properties... + ] + } + } + + +### Users no longer invited to room + +This can be used to notify users that they are no longer invited to a room. + +Message format (Backend -> Server) + + { + "type": "disinvite" + "disinvite" { + "userids": [ + ...list of user ids that are no longer invited to the room... + ], + "alluserids": [ + ...list of all user ids that still invited to the room... + ] + } + } + + +### Room updated + +This can be used to notify about changes to a room. The room properties are the +same as described in section "Join room" above. + +Message format (Backend -> Server) + + { + "type": "update" + "update" { + "userids": [ + ...list of user ids that are invited to the room... + ], + "properties": [ + ...additional room properties... + ] + } + } + + +### Room deleted + +This can be used to notify about a deleted room. All sessions currently +connected to the room will leave the room. + +Message format (Backend -> Server) + + { + "type": "delete" + "delete" { + "userids": [ + ...list of user ids that were invited to the room... + ] + } + } + + +### Participants changed + +This can be used to notify about changed participants. + +Message format (Backend -> Server) + + { + "type": "participants" + "participants" { + "changed": [ + ...list of users that were changed... + ], + "users": [ + ...list of users in the room... + ] + } + } + + +### In call state of participants changed + +This can be used to notify about participants that changed their `inCall` flag. + +Message format (Backend -> Server) + + { + "type": "incall" + "incall" { + "incall": new-incall-state, + "changed": [ + ...list of users that were changed... + ], + "users": [ + ...list of users in the room... + ] + } + } + + +### Send an arbitrary room message + +This can be used to send arbitrary messages to participants in a room. It is +currently used to notify about new chat messages. + +Message format (Backend -> Server) + + { + "type": "message" + "message" { + "data": { + ...arbitrary object to sent to clients... + } + } + }