-module(livery_wt).
-moduledoc """
WebTransport integration on top of the `webtransport` library.
A handler that wants to accept a WebTransport session (initiated
by an HTTP extended CONNECT with `:protocol = webtransport`) calls
`upgrade/3`. The function dispatches to the adapter's `accept_wt/4`
helper, which reconstructs the CONNECT pseudo-headers and calls
`webtransport:accept/4`. On success the stream/session belongs to
the `webtransport` session process and `upgrade/3` returns the
`taken_over` sentinel response.
```erlang
my_wt_route(Req) ->
livery_wt:upgrade(Req, my_wt_handler, #{}).
```
`my_wt_handler` implements the `webtransport_handler` behaviour
(from `erlang-webtransport`).
WebTransport runs only over H2 (extended CONNECT, RFC 9220-style)
and H3. Calling `upgrade/3` on H1 returns `501 Not Implemented`.
The listener must advertise the WebTransport settings. Merge
`webtransport:h3_settings/0` (or `h2_settings/0`) into the listener
options so the adapter forwards the H3/H2 SETTINGS, datagram
support, and WT stream routing to the wire library:
```erlang
Opts = maps:merge(webtransport:h3_settings(), #{
cert => Cert, key => Key, stack => Stack,
handler => fun(Req) -> livery_wt:upgrade(Req, my_handler, #{}) end
}),
livery_h3:start(Opts).
```
End-to-end bidi-stream and datagram echo over a real session is
covered by `livery_wt_SUITE` (needs `webtransport` >= 0.2.3, where
`accept/4` works from Livery's per-request worker process).
""".
-include("livery.hrl").
-export([upgrade/3]).
-export_type([handler_module/0, handler_opts/0]).
-type handler_module() :: module().
-type handler_opts() :: term().
-doc """
Accept a WebTransport session on the current request.
`HandlerMod` implements `webtransport_handler`. Returns the
`taken_over` sentinel on success, `501` on H1, or `4xx/5xx` text
when the handshake is rejected.
""".
-spec upgrade(livery_req:req(), handler_module(), handler_opts()) ->
livery_resp:resp().
upgrade(Req, HandlerMod, Opts) ->
Adapter = livery_req:adapter(Req),
case adapter_transport(Adapter) of
undefined ->
livery_resp:text(
501,
<<"WebTransport not supported on this protocol">>
);
Transport ->
case Adapter:accept_wt(Transport, Req, HandlerMod, Opts) of
{ok, _Session} ->
#livery_resp{status = 200, body = taken_over};
{error, not_connect_method} ->
livery_resp:text(400, <<"not a CONNECT request">>);
{error, {rejected, Status}} ->
livery_resp:text(Status, <<"webtransport rejected">>);
{error, Reason} ->
livery_resp:text(
500,
iolist_to_binary([
<<"webtransport upgrade failed: ">>,
format_reason(Reason)
])
)
end
end.
-spec adapter_transport(module()) -> h2 | h3 | undefined.
adapter_transport(livery_h2) -> h2;
adapter_transport(livery_h3) -> h3;
adapter_transport(_) -> undefined.
-spec format_reason(term()) -> iodata().
format_reason(B) when is_binary(B) -> B;
format_reason(A) when is_atom(A) -> atom_to_binary(A);
format_reason(Other) -> io_lib:format("~p", [Other]).