Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/guide/client-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,15 +665,16 @@ The timeout bounds the requests as a whole, not each read, so a server that send

Endpoints have to be `https` URLs, as WHIP and WHEP ask, except on the machine the application runs on: there plain `http` will do, for a media server beside it, at `localhost`, an address in 127.0.0.0/8 or `::1`. A name that merely resolves to one of them does not count, since it may resolve elsewhere later. The same holds for the session an endpoint names and for where a redirect points.

The bearer token goes to the endpoint, and along with the deletion to the session the endpoint names in its `Location` header. That session may be on another host, but an `https` endpoint cannot name a plain `http` one. The offer follows redirects with status 307 and 308, as WHIP and WHEP ask of clients, such as those of a load balancer: up to five of them, all within the timeout, and the token goes along. A redirect from `https` to plain `http` fails the session, and so does one with status 301, 302 or 303, which would turn the offer into a `GET`. The deletion follows no redirect; one fails it with its status.
The credentials of a session, its bearer token or its Basic user and password, go to the endpoint, and along with the deletion to the session the endpoint names in its `Location` header. That session may be on another host, but an `https` endpoint cannot name a plain `http` one. The offer follows redirects with status 307 and 308, as WHIP and WHEP ask of clients, such as those of a load balancer: up to five of them, all within the timeout, and the credentials go along. A redirect from `https` to plain `http` fails the session, and so does one with status 301, 302 or 303, which would turn the offer into a `GET`. The deletion follows no redirect; one fails it with its status.

Servers that ask for a user and password rather than a token, such as MediaMTX, take them as the token, the way OBS Studio sends them: `bearerToken("user:password")` sends `Authorization: Bearer user:password`.
Servers that ask for a user and password rather than a token, such as MediaMTX, take them by HTTP Basic authentication (RFC 7617): `basicAuth("user", "password")` sends `Authorization: Basic` with both, in UTF-8, on every request of the session, without waiting to be asked. The user must not contain a colon. Of `bearerToken` and `basicAuth`, the one called last counts. MediaMTX also takes them as a token, the way OBS Studio sends them, `bearerToken("user:password")`. Other schemes, such as Digest, are not supported.

Requests go through Java's `HttpClient`, with the default `SSLContext` and the HTTP proxy Java is configured with, the first the default `ProxySelector` names: `-Dhttps.proxyHost` and `-Dhttps.proxyPort`, `-Dhttp.proxyHost`, or `-Djava.net.useSystemProxies=true`. `HttpClient` takes no SOCKS proxies. A proxy that asks for credentials gets Basic ones from the default `Authenticator`, but only for sessions without a bearer token: `HttpClient` drops the token of a request once it answers for credentials itself. As in Java, Basic is not used for the tunnel of an `https` request unless `jdk.http.auth.tunneling.disabledSchemes` allows it. Other schemes, such as Digest, NTLM and Negotiate, are not supported.
Requests go through Java's `HttpClient`, with the default `SSLContext` and the HTTP proxy Java is configured with, the first the default `ProxySelector` names: `-Dhttps.proxyHost` and `-Dhttps.proxyPort`, `-Dhttp.proxyHost`, or `-Djava.net.useSystemProxies=true`. `HttpClient` takes no SOCKS proxies. A proxy that asks for credentials gets Basic ones from the default `Authenticator`, but only for sessions without credentials of their own: `HttpClient` drops the Authorization header of a request once it answers for credentials itself. As in Java, Basic is not used for the tunnel of an `https` request unless `jdk.http.auth.tunneling.disabledSchemes` allows it. Other schemes, such as Digest, NTLM and Negotiate, are not supported.

| Builder method | What it sets |
|---|---|
| `bearerToken(String)` | The token sent as `Authorization: Bearer`; for many services, the stream key, for MediaMTX `user:password`. |
| `bearerToken(String)` | The token sent as `Authorization: Bearer`; for many services, the stream key. |
| `basicAuth(String, String)` | A user and password sent as `Authorization: Basic`, as MediaMTX takes them; replaces a bearer token, and the other way round. |
| `iceServer(String...)` | A STUN or TURN server without credentials, overriding endpoint-provided servers. |
| `turnServer(String, String, String)` | A TURN URL, username and password, overriding endpoint-provided servers. |
| `gathering(Gathering)` | When the initial offer is sent: `TRICKLE` by default, or `COMPLETE` to gather first. |
Expand Down
20 changes: 11 additions & 9 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/HttpCall.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ final class HttpCall {

private final String method;

private final String bearerToken;
private final String authorization;

private final Duration timeout;

Expand Down Expand Up @@ -126,14 +126,16 @@ final class HttpCall {


/**
* @param timeout how long the request may take, from now on, until its
* response has been read; redirects included.
* @param authorization the value of the Authorization header, such as
* {@code Bearer <token>}, or null for none.
* @param timeout how long the request may take, from now on, until
* its response has been read; redirects included.
*/
HttpCall(HttpClient client, URI uri, String method, String bearerToken, Duration timeout) {
HttpCall(HttpClient client, URI uri, String method, String authorization, Duration timeout) {
this.client = client;
this.uri = uri;
this.method = method;
this.bearerToken = bearerToken;
this.authorization = authorization;
this.timeout = timeout;
this.start = System.nanoTime();

Expand All @@ -160,7 +162,7 @@ final class HttpCall {
* Basic credentials with those of the default {@link
* Authenticator} of the moment. A client that does
* drops the Authorization header of its requests, so
* it cannot send a bearer token.
* it cannot send the credentials of a session.
*
* @return the client.
*/
Expand Down Expand Up @@ -220,7 +222,7 @@ HttpCall readBodies(Set<Integer> statuses) {

/**
* Follows redirects with status 307 and 308, up to {@link #MAX_REDIRECTS}
* of them, with the bearer token. One from https to plain http fails.
* of them, with the credentials. One from https to plain http fails.
*
* @return this request.
*/
Expand Down Expand Up @@ -302,8 +304,8 @@ private void exchange(URI target, String contentType, byte[] content, int maxBod
try {
HttpRequest.Builder builder = HttpRequest.newBuilder(target).timeout(Duration.ofNanos(left));

if (bearerToken != null) {
builder.header("Authorization", "Bearer " + bearerToken);
if (authorization != null) {
builder.header("Authorization", authorization);
}

headers.forEach(builder::header);
Expand Down
10 changes: 5 additions & 5 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/HttpIce.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ final class HttpIce {

private final PeerImpl peer;

private final String bearerToken;
private final String authorization;

private final Duration timeout;

Expand Down Expand Up @@ -110,11 +110,11 @@ final class HttpIce {
private boolean closed;


HttpIce(WebRtc rtc, HttpClient client, PeerImpl peer, String bearerToken, Duration timeout) {
HttpIce(WebRtc rtc, HttpClient client, PeerImpl peer, String authorization, Duration timeout) {
this.rtc = rtc;
this.client = client;
this.peer = peer;
this.bearerToken = bearerToken;
this.authorization = authorization;
this.timeout = timeout;

peer.onLocalCandidates(this::candidate, this::gathered);
Expand Down Expand Up @@ -191,7 +191,7 @@ CompletableFuture<Void> restart() {
pending.clear();
gathered = false;

request = new HttpCall(client, session, "PATCH", bearerToken, timeout).header("If-Match", "*");
request = new HttpCall(client, session, "PATCH", authorization, timeout).header("If-Match", "*");
restartRequest = request;
}

Expand Down Expand Up @@ -343,7 +343,7 @@ private void flush() {
pending.clear();
gathered = false;

request = new HttpCall(client, session, "PATCH", bearerToken, timeout);
request = new HttpCall(client, session, "PATCH", authorization, timeout);

if (etag != null) {
request.header("If-Match", etag);
Expand Down
69 changes: 49 additions & 20 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/HttpSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -71,7 +72,7 @@ abstract class HttpSession implements AutoCloseable {

private final URI resource;

private final String bearerToken;
private final String authorization;

private final Duration timeout;

Expand All @@ -85,11 +86,11 @@ abstract class HttpSession implements AutoCloseable {
private final CompletableFuture<Void> closed = new CompletableFuture<>();


HttpSession(WebRtc rtc, Peer peer, URI resource, String bearerToken, Duration timeout, HttpIce ice) {
HttpSession(WebRtc rtc, Peer peer, URI resource, String authorization, Duration timeout, HttpIce ice) {
this.rtc = rtc;
this.peer = peer;
this.resource = resource;
this.bearerToken = bearerToken;
this.authorization = authorization;
this.timeout = timeout;
this.ice = ice;

Expand Down Expand Up @@ -139,7 +140,7 @@ public CompletableFuture<Void> closeAsync() {
}

CompletableFuture<Throwable> peerClosed = peer.closeAsync().handle((ignored, error) -> error);
CompletableFuture<WebRtcException> deleted = discard(rtc, resource, bearerToken, timeout);
CompletableFuture<WebRtcException> deleted = discard(rtc, resource, authorization, timeout);

deleted.thenAcceptBoth(peerClosed, (deleteError, peerError) -> {
Throwable error = PeerImpl.unwrap(peerError);
Expand Down Expand Up @@ -204,13 +205,13 @@ public void close() {
* @return the future handed to the application.
*/
static <S extends HttpSession> CompletableFuture<S> start(WebRtc rtc, PeerImpl peer, URI endpoint,
String bearerToken, Duration timeout, CompletableFuture<?> prepared, Gathering gathering,
String authorization, Duration timeout, CompletableFuture<?> prepared, Gathering gathering,
Protocol protocol, Factory<S> factory) {
CompletableFuture<Void> abandoned = new CompletableFuture<>();
HttpIce ice = new HttpIce(rtc, rtc.httpClient(bearerToken != null), peer, bearerToken, timeout);
CompletableFuture<S> session = peer.toPublic(negotiate(rtc, peer, endpoint, bearerToken, timeout, prepared,
HttpIce ice = new HttpIce(rtc, rtc.httpClient(authorization != null), peer, authorization, timeout);
CompletableFuture<S> session = peer.toPublic(negotiate(rtc, peer, endpoint, authorization, timeout, prepared,
gathering, protocol, ice, abandoned)
.thenApply(resource -> factory.create(rtc, peer, resource, bearerToken, timeout, ice)),
.thenApply(resource -> factory.create(rtc, peer, resource, authorization, timeout, ice)),
HttpSession::close);

// Once the session was handed out, this finds the negotiation finished.
Expand All @@ -237,7 +238,7 @@ static <S extends HttpSession> CompletableFuture<S> start(WebRtc rtc, PeerImpl p
*
* @return a future with the resource the server named, or null.
*/
private static CompletableFuture<URI> negotiate(WebRtc rtc, PeerImpl peer, URI endpoint, String bearerToken,
private static CompletableFuture<URI> negotiate(WebRtc rtc, PeerImpl peer, URI endpoint, String authorization,
Duration timeout, CompletableFuture<?> prepared, Gathering gathering, Protocol protocol, HttpIce ice,
CompletableFuture<?> abandoned) {
CompletableFuture<URI> result = new CompletableFuture<>();
Expand All @@ -246,10 +247,10 @@ private static CompletableFuture<URI> negotiate(WebRtc rtc, PeerImpl peer, URI e
// session that connected, or whoever deletes it.
AtomicReference<URI> resource = new AtomicReference<>();
AtomicBoolean finished = new AtomicBoolean();
HttpClient client = rtc.httpClient(bearerToken != null);
HttpClient client = rtc.httpClient(authorization != null);
// Made now, so that it ends with the session's deadline at the latest,
// however long the offer takes.
HttpCall offerRequest = new HttpCall(client, endpoint, "POST", bearerToken, timeout)
HttpCall offerRequest = new HttpCall(client, endpoint, "POST", authorization, timeout)
.followRedirects()
.retry(rtc.timer, protocol.retried)
.readBodies(protocol.counterOffers ? COUNTER_OFFER : Collections.emptySet());
Expand All @@ -272,7 +273,7 @@ private static CompletableFuture<URI> negotiate(WebRtc rtc, PeerImpl peer, URI e
result.completeExceptionally(PeerImpl.asWebRtcException(error,
"The session with " + endpoint + " could not be set up"));

discard(rtc, resource.getAndSet(null), bearerToken, timeout);
discard(rtc, resource.getAndSet(null), authorization, timeout);
};

ScheduledFuture<?> deadline = rtc.timer.schedule(() -> fail.accept(new WebRtcException(
Expand All @@ -293,7 +294,7 @@ private static CompletableFuture<URI> negotiate(WebRtc rtc, PeerImpl peer, URI e
// The session ended while the offer was out,
// after the server named what it created, which
// then has no one else to delete it.
discard(rtc, resource.getAndSet(null), bearerToken, timeout);
discard(rtc, resource.getAndSet(null), authorization, timeout);
}
})
.thenCompose(reply -> (gathering == Gathering.COMPLETE
Expand Down Expand Up @@ -322,7 +323,7 @@ private static CompletableFuture<URI> negotiate(WebRtc rtc, PeerImpl peer, URI e
// The rest of the session's time, which the answer has.
Duration left = timeout.minusNanos(System.nanoTime() - started);

return answerCounterOffer(rtc, client, peer, reply, bearerToken,
return answerCounterOffer(rtc, client, peer, reply, authorization,
left.isNegative() || left.isZero() ? Duration.ofNanos(1) : left, resource, ice,
request, finished);
}))
Expand Down Expand Up @@ -395,7 +396,7 @@ private static CompletableFuture<Reply> post(HttpCall request, URI endpoint, Str
* @param timeout what is left of the session's timeout.
*/
private static CompletableFuture<Void> answerCounterOffer(WebRtc rtc, HttpClient client, PeerImpl peer,
Reply reply, String bearerToken, Duration timeout, AtomicReference<URI> resource, HttpIce ice,
Reply reply, String authorization, Duration timeout, AtomicReference<URI> resource, HttpIce ice,
AtomicReference<HttpCall> request, AtomicBoolean finished) {
return peer.answerInstead(new RTCSessionDescription(RTCSdpType.OFFER, reply.sdp), Gathering.COMPLETE)
.thenCompose(answer -> {
Expand All @@ -404,7 +405,7 @@ private static CompletableFuture<Void> answerCounterOffer(WebRtc rtc, HttpClient
+ " expired before it was answered");
}

HttpCall answerRequest = new HttpCall(client, reply.session, "PATCH", bearerToken, timeout);
HttpCall answerRequest = new HttpCall(client, reply.session, "PATCH", authorization, timeout);
request.set(answerRequest);

if (finished.get()) {
Expand Down Expand Up @@ -544,7 +545,7 @@ private static final class Reply {
*/
interface Factory<S extends HttpSession> {

S create(WebRtc rtc, Peer peer, URI resource, String bearerToken, Duration timeout, HttpIce ice);
S create(WebRtc rtc, Peer peer, URI resource, String authorization, Duration timeout, HttpIce ice);
}

/**
Expand All @@ -554,7 +555,7 @@ interface Factory<S extends HttpSession> {
*
* @return a future with the failure reported, or null.
*/
private static CompletableFuture<WebRtcException> discard(WebRtc rtc, URI resource, String bearerToken,
private static CompletableFuture<WebRtcException> discard(WebRtc rtc, URI resource, String authorization,
Duration timeout) {
if (resource == null) {
return CompletableFuture.completedFuture(null);
Expand All @@ -563,7 +564,7 @@ private static CompletableFuture<WebRtcException> discard(WebRtc rtc, URI resour
HttpCall request;

try {
request = new HttpCall(rtc.httpClient(bearerToken != null), resource, "DELETE", bearerToken, timeout);
request = new HttpCall(rtc.httpClient(authorization != null), resource, "DELETE", authorization, timeout);
}
catch (RejectedExecutionException e) {
return CompletableFuture.completedFuture(leftOnServer(rtc, resource));
Expand Down Expand Up @@ -617,7 +618,7 @@ private static WebRtcException leftOnServer(WebRtc rtc, URI resource) {

/**
* Resolves the resource the server named for a session. It may be on
* another host, as WHIP and WHEP allow, but the bearer token goes along to
* another host, as WHIP and WHEP allow, but the credentials go along to
* delete it, so an https endpoint cannot name a plain http resource.
*/
static URI resourceOf(URI endpoint, String location) {
Expand Down Expand Up @@ -708,6 +709,34 @@ static void requireContentType(HttpCall.Response response, String expected) {
}
}

/**
* @return the value of an Authorization header with a bearer token (RFC
* 6750), as WHIP and WHEP ask clients to support.
*/
static String bearer(String token) {
return "Bearer " + requireNonNull(token, "token");
}

/**
* @return the value of an Authorization header with Basic credentials (RFC
* 7617), in UTF-8.
*
* @throws IllegalArgumentException if the user contains a colon, which
* Basic cannot tell from the one that
* separates the password.
*/
static String basic(String user, String password) {
requireNonNull(user, "user");
requireNonNull(password, "password");

if (user.indexOf(':') >= 0) {
throw new IllegalArgumentException("The user of Basic authentication must not contain a colon");
}

return "Basic " + Base64.getEncoder().encodeToString((user + ":" + password)
.getBytes(StandardCharsets.UTF_8));
}

/**
* Checks the codec names given to a builder.
*
Expand Down
9 changes: 5 additions & 4 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/WebRtc.java
Original file line number Diff line number Diff line change
Expand Up @@ -605,22 +605,23 @@ private void shutdownPools() {
}

/**
* @param bearerToken whether the requests carry a bearer token.
* @param authorizes whether the requests carry credentials of their own,
* in the Authorization header.
*
* @return the client of WHIP and WHEP requests.
*
* @throws RejectedExecutionException once the runtime has stopped its
* threads.
*/
HttpClient httpClient(boolean bearerToken) {
int index = bearerToken ? 0 : 1;
HttpClient httpClient(boolean authorizes) {
int index = authorizes ? 0 : 1;

synchronized (httpLock) {
if (httpStopped) {
throw new RejectedExecutionException("The runtime is closed");
}
if (httpClients[index] == null) {
httpClients[index] = HttpCall.newClient(!bearerToken);
httpClients[index] = HttpCall.newClient(!authorizes);
}

return httpClients[index];
Expand Down
Loading
Loading