4. API¶
- The Federation API consists of two layers:
Between federators
Between other components
4.1. Qualified Identifiers and Names¶
The federated (and consequently distributed) architecture is reflected in the structure of the various identifiers and names used in the API. Before federation, identifiers were only unique in the context of a single backend; for federation, they are made globally unique by combining them with the federation domain of their backend. We call these combined identifiers qualified identifiers. While other parts of some identifiers or names may change, the domain name (i.e. the qualifying part) is static.
In particular, we use the following identifiers throughout the API:
Qualified User ID (QUID): user_uuid@backend-domain.com
Qualified User Name (QUN): user_name@backend-domain.com
Qualified Client ID (QDID) attached to a QUID: client_uuid.user_uuid@backend-domain.com
Qualified Conversation/Group ID (QCID/QGID): backend-domain.com/groups/group_uuid
Qualified Team ID (QTID): backend-domain.com/teams/team_uuid
While the canonical representation for purposes of visualization is as displayed above, the API often decomposes the qualified identifiers into an (unqualified) id and a domain name. In the code and API documentation, we sometimes call a username a “handle” and a qualified username a “qualified handle”.
Besides the above names and identifiers, there are also user display names (sometimes also referred to as “profile names”), which are not unique on the user’s backend, can be changed by the user at any time and are not qualified.
4.2. API between Federators¶
The layer between federators acts as an envelope for communication between other components of wire server. It uses Protocol Buffers (protobuf from here onwards) for serialization over gRPC. The latest protobuf schema can be inspected at the wire-server repository.
All gRPC calls are made via a mutually authenticated TLS connection and subject to a general, as well as a per-request authorization step.
The Inward
service defined in the schema is used between federators. It
supports one rpc called call
which requires a Request
and returns an
InwardResponse
. These objects looks like this:
message Request {
Component component = 1;
bytes path = 2;
bytes body = 3;
string originDomain = 4
}
enum Component {
Brig = 0;
Galley = 1;
}
message InwardResponse {
oneof response {
InwardError err = 1;
bytes body = 2;
}
}
message InwardError {
enum ErrorType {
IOther = 0;
IInvalidDomain = 1;
IFederationDeniedByRemote = 2;
IInvalidEndpoint = 3;
IForbiddenEndpoint = 4;
}
ErrorType type = 1;
string msg = 2;
}
The component
field in Request
tells the federator which components this
request is meant for and the rest of the arguments are details of the HTTP
request which must be made against the component. It intentionally supports a
restricted set of parameters to ensure that the API is simple.
4.3. API From Components to Federator¶
Between two federated backends, the components talk to each other via their
respective federators. When making the call to the federator, the components use
protobuf over gRPC. They call the Outward
service, which also supports one
rpc called call
. This rpc requires a FederatedRequest
object, which
contains a Request
object as defined above, as well as the domain of the
destination federator. The rpc returns an OutwardResponse
, which can either
contains a body with the returned information or an OutwardError
, these
objects look like this:
message FederatedRequest {
string domain = 1;
Request request = 2;
}
message OutwardResponse {
oneof response {
OutwardError err = 1;
bytes body = 2;
}
}
message OutwardError {
enum ErrorType {
RemoteNotFound = 0;
DiscoveryFailed = 1;
ConnectionRefused = 2;
TLSFailure = 3;
InvalidCertificate = 4;
VersionMismatch = 5;
FederationDeniedByRemote = 6;
FederationDeniedLocally = 7;
RemoteFederatorError = 8;
InvalidRequest = 9;
}
ErrorType type = 1;
ErrorPayload payload = 2;
}
message ErrorPayload {
string label = 1;
string msg = 2;
}
4.4. API From Federator to Components¶
The components expose a REST API over HTTP to be consumed by the federator. All
the paths start with /federation
. When a federator recieves a request like
this (shown as JSON for convenience):
{
"component": "Brig",
"path": "federation/get-user-by-handle",
"body": "\"janedoe\"",
"originDomain": "somedomain.example.com"
}
The federator connects to Brig and makes an HTTP request which looks like this:
> POST /federation/get-user-by-handle
> Wire-Origin-Domain: somedomain.example.com
> Content-Type: application/json
>
> "janedoe"
The /federation
prefix to the path allows the component to distinguish
federated requests from requests by clients or other local components.
If this request succeeds with any status, the response is encoded as the
InwardResponse
object and returned as a response to the Inward.call
gRPC
call.
Note, that before the path
field of the Request
is concatenated with
/federation
and used as a component of the HTTP request, its segments are
normalized as described in Section 6.2.2.3 of RFC 3986 to prevent
path-traversal attacks such as /federation/../users/by-handle
.
4.5. List of Federation APIs exposed by Components¶
Each component of the backend provides an API towards the federator for access by other backends. For example on how these APIs are used, see the section on end-to-end flows.
Note
This reflects status of API endpoints as of 2021-06-25. For latest APIs please refer to the corresponding source code linked in the individual section.
4.5.1. Brig¶
In its current state, the primary purpose of the Brig API is to allow users of remote backends to create conversations with the local users of the backend.
get-user-by-handle
: Given a handle, return the user profile corresponding to that handle.get-users-by-ids
: Given a list of user ids, return the list of corresponding user profiles.claim-prekey
: Given a user id and a client id, return a Proteus pre-key belonging to that user.claim-prekey-bundle
: Given a user id, return a prekey for each of the user’s clients.claim-multi-prekey-bundle
: Given a list of user ids, return the lists of their respective clients.search-users
: Given a term, search the user database for matches w.r.t. that term.get-user-clients
: Given a list of user ids, return the lists of clients of each of the users.
See the brig source code for the current list of federated endpoints of the Brig, as well as their precise inputs and outputs.
4.5.2. Galley¶
Each backend keeps a record of the conversations that each of its members is a part of. The purpose of the Galley API is to allow backends to synchronize the state of the conversations of their members.
register-conversation
: Given a name and a list of conversation members, create a conversation locally. This is used to inform another backend of a new conversation that involves their local user.get-conversations
: Given a qualified user id and a list of conversation ids, return the details of the conversations. This allows a remote backend to query conversation metadata of their local user from this backend. To avoid metadata leaks, the backend will check that the domain of the given user corresponds to the domain of the backend sending the request.update-conversation-memberships
: Given a qualified user id and a qualified conversation id, update the conversation details locally with the other data provided. This is used to alert remote backend of updates in the conversation metadata of conversations that one of their local users is involved in.receive-message
: Given (sender, recipients, message payloads), propagate a message to local users. This is used whenever there is a remote user in a conversation (see end-to-end flows).send-message
: Given a sender and a raw message request, send a message to a conversation owned by another backend. This is used when the user sending a message is not on the same backend as the conversation the message is sent in.
See the galley source code for the current list of federated endpoints of the Galley, as well as their precise inputs and outputs.
4.6. End-to-End Flows¶
4.6.1. User Discovery¶
In this flow, the user A at backend-a.com tries to search for user B at backend-b.com.
User A@backend-a.com enters the qualified user name of the target user B@backend-b.com into the search field of their Wire client.
The client issues a query to
/search/contacts
searching for B at backend-b.com.A’s backend queries the
search-users
endpoint of B’s backend for B.B’s backend replies with with B’s user name and qualified handle.
A’s backend forwards that information to A’s client.
4.6.2. Conversation Establishment¶
After having discovered user B at backend-b.com, user A at backend-a.com wants to establish a conversation with B.
From the search results of a user discovery process, A chooses to create a conversation with B.
A’s client issues a
/users/backend-b.com/B/prekeys
query to A’s backend.A’s backend queries the
claim-prekey-bundle
endpoint of B’s backend using B’s user id.B’s backend replies with a prekey bundle for each of B’s clients.
A’s backend forwards that information to A’s client.
A’s client queries the
/conversations
endpoint of its backend using B’s user id.A’s backend creates the conversation locally and queries the
register-conversation
endpoint of B’s backend to inform it about the new conversation, including the conversation metadata in the request.B’s backend registers the conversation locally and confirms the query.
B’s backend notifies B’s client of the creation of the conversation.
4.6.3. Message Sending (A)¶
Having established a conversation with user B at backend-b.com, user A at backend-a.com wants to send a message to user B.
In a conversation conv-1@backend-a.com on A’s backend with users A@backend-a.com and B@backend-b.com, A sends a message by using the
/conversations/backend-a.com/conv-1/proteus/messages
endpoint on A’s backend.A’s backend will check if A included all necessary user devices in their request. For that it will make a
get-user-clients
request to B’s backend. The returned list of clients will be checked to match against the list of clients the message was encrypted for.A’s backend will send the message to all clients of those users on A’s backend part of the conversation as usual,
A’s backend will query the
receive-message
endpoint on B’s backend.B’s backend will propagate the message to all users on B.
4.6.4. Message Sending (B)¶
Having received a message from user A at backend-a.com, user B at backend-b.com wants send a reply.
In a conversation conv-1@backend-a.com on A’s backend with users A@backend-a.com and B@backend-b.com, B sends a message by using the
/conversations/backend-a.com/conv-1/proteus/messages
endpoint on B’s backend.B’s backend will query the
send-message
endpoint on A’s backend. Steps 3-6 below are essentially the same as steps 2-5 in Message Sending (A)A’s backend will check if B included all necessary user devices in their request. For that it will make a
get-user-clients
request to B’s backend. The returned list of clients will be checked to match against the list of clients the message was encrypted for.A’s backend will send the message to all clients of those users on A’s backend part of the conversation as usual,
A’s backend will query the
receive-message
endpoint on B’s backend.B’s backend will propagate the message to all users on B.