From 172800967c2d53251d7cb1015e3c957c5b065bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 5 Dec 2022 17:22:09 +0100 Subject: Document Websocket subprotocol negotiation --- doc/src/manual/gun.asciidoc | 33 +++++++--- doc/src/manual/gun.ws_upgrade.asciidoc | 21 ++++++- doc/src/manual/gun_app.asciidoc | 1 + doc/src/manual/gun_ws_protocol.asciidoc | 108 ++++++++++++++++++++++++++++++++ ebin/gun.app | 2 +- src/gun.erl | 4 +- src/gun_ws_h.erl | 1 + src/gun_ws_protocol.erl | 25 ++++++++ 8 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 doc/src/manual/gun_ws_protocol.asciidoc create mode 100644 src/gun_ws_protocol.erl diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc index 9ad3e8e..40559fb 100644 --- a/doc/src/manual/gun.asciidoc +++ b/doc/src/manual/gun.asciidoc @@ -525,12 +525,14 @@ detail. [source,erlang] ---- ws_opts() :: #{ - closing_timeout => timeout(), - compress => boolean(), - flow => pos_integer(), - keepalive => timeout(), - protocols => [{binary(), module()}], - silence_pings => boolean() + closing_timeout => timeout(), + compress => boolean(), + default_protocol => module(), + flow => pos_integer(), + keepalive => timeout(), + protocols => [{binary(), module()}], + silence_pings => boolean(), + user_opts => any() } ---- @@ -550,6 +552,11 @@ Whether to enable permessage-deflate compression. This does not guarantee that compression will be used as it is the server that ultimately decides. Defaults to false. +default_protocol (gun_ws_h):: + +Default protocol module when no Websocket subprotocol is +negotiated. + flow - see below:: The initial flow control value for the Websocket connection. @@ -563,8 +570,10 @@ protocols ([]):: A non-empty list enables Websocket protocol negotiation. The list of protocols will be sent in the sec-websocket-protocol -request header. The handler module interface is currently -undocumented and must be set to `gun_ws_h`. +request header. The given module must follow the +link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] interface. +Gun comes with a default interface in `gun_ws_h` that may +be reused for negotiated protocols. silence_pings (true):: @@ -572,10 +581,16 @@ Whether the ping and pong frames should be sent to the user. In all cases Gun will automatically send a pong frame back when receiving a ping. -// @todo Document default_protocol and user_opts. +user_opts - see below:: + +Additional options that are not in use by Gun unless a custom +Websocket subprotocol is configured and negotiated. +By default no user option is defined. == Changelog +* *2.0*: The `default_protocol` and `user_opts` Websocket + options were added. * *2.0*: The `stream_ref()` type was added. * *2.0*: The option `cookie_store` was added. It can be used to configure a cookie store that Gun will use diff --git a/doc/src/manual/gun.ws_upgrade.asciidoc b/doc/src/manual/gun.ws_upgrade.asciidoc index b553f3b..c6e3850 100644 --- a/doc/src/manual/gun.ws_upgrade.asciidoc +++ b/doc/src/manual/gun.ws_upgrade.asciidoc @@ -20,7 +20,7 @@ ws_upgrade(ConnPid, Path, Headers, WsOpts) ConnPid :: pid() Path :: iodata() Headers :: gun:req_headers() -WsOpts :: gun:ws_opts +WsOpts :: gun:ws_opts() StreamRef :: gun:stream_ref() ---- @@ -39,6 +39,11 @@ Gun does not currently support Websocket over HTTP/2. By default Gun will take the Websocket options from the connection's `ws_opts`. +Websocket subprotocol negotiation is enabled when +the `protocols` option is given. It takes a subprotocol +name and a module implementing the +link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] behavior. + == Arguments ConnPid:: @@ -92,9 +97,21 @@ StreamRef = gun:ws_upgrade(ConnPid, "/ws", [], #{ }). ---- +.Upgrade to Websocket with protocol negotiation +[source,erlang] +---- +StreamRef = gun:ws_upgrade(ConnPid, "/ws", [], #{ + protocols => [ + {<<"mqtt">>, gun_ws_mqtt_h}, + {<<"v12.stomp">>, gun_ws_stomp_h} + ] +}). +---- + == See also link:man:gun(3)[gun(3)], link:man:gun:ws_send(3)[gun:ws_send(3)], link:man:gun_upgrade(3)[gun_upgrade(3)], -link:man:gun_ws(3)[gun_ws(3)] +link:man:gun_ws(3)[gun_ws(3)], +link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] diff --git a/doc/src/manual/gun_app.asciidoc b/doc/src/manual/gun_app.asciidoc index 168d0e9..ca05594 100644 --- a/doc/src/manual/gun_app.asciidoc +++ b/doc/src/manual/gun_app.asciidoc @@ -19,6 +19,7 @@ to the server and reconnects automatically when necessary. * link:man:gun_cookies(3)[gun_cookies(3)] - Cookie store engine * link:man:gun_cookies_list(3)[gun_cookies_list(3)] - Cookie store backend: in-memory, per connection * link:man:gun_event(3)[gun_event(3)] - Events +* link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] - Websocket subprotocols == Dependencies diff --git a/doc/src/manual/gun_ws_protocol.asciidoc b/doc/src/manual/gun_ws_protocol.asciidoc new file mode 100644 index 0000000..417ba94 --- /dev/null +++ b/doc/src/manual/gun_ws_protocol.asciidoc @@ -0,0 +1,108 @@ += gun_ws_protocol(3) + +== Name + +gun_ws_protocol - Websocket subprotocols + +== Description + +The `gun_ws_protocol` module provides the callback interface +and types for implementing Websocket subprotocols. + +== Callbacks + +Websocket subprotocols implement the following interface. + +=== init + +[source,erlang] +---- +init(ReplyTo, StreamRef, Headers, Opts) -> {ok, State} + +ReplyTo :: pid() +StreamRef :: reference() +Headers :: cow_http:headers() +Opts :: gun:ws_opts() +State :: protocol_state() +---- + +Initialize the Websocket protocol. + +ReplyTo:: + +The pid of the process that owns the stream and to +which messages will be sent to. + +StreamRef:: + +The reference for the stream. Must be sent in messages +to distinguish between different streams. + +Headers:: + +Headers that were sent in the response establishing +the Websocket connection. + +Opts:: + +Websocket options. Custom options can be provided in +the `user_opts` key. + +State:: + +State for the protocol. + +=== handle + +[source,erlang] +---- +handle(Frame, State) -> {ok, FlowDec, State} + +Frame :: cow_ws:frame() +State :: protocol_state() +FlowDec :: non_neg_integer() +---- + +Handle a Websocket frame. + +This callback may receive fragmented frames depending +on the protocol and may need to rebuild the full +frame to process it. + +Frame:: + +Websocket frame. + +State:: + +State for the protocol. + +FlowDec:: + +How many messages were sent. Used to update the flow +control state when the feature is enabled. + +== Types + +=== protocol_state() + +[source,erlang] +---- +protocol_state() :: any() +---- + +State for the protocol. + +As this part of the implementation of the protocol +the type may differ between different Websocket +protocol modules. + +== Changelog + +* *2.0*: Module introduced. + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun(3)[gun(3)], +link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)] diff --git a/ebin/gun.app b/ebin/gun.app index 6341631..2b8ee7b 100644 --- a/ebin/gun.app +++ b/ebin/gun.app @@ -1,7 +1,7 @@ {application, 'gun', [ {description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."}, {vsn, "2.0.0-rc.2"}, - {modules, ['gun','gun_app','gun_conns_sup','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_tunnel','gun_ws','gun_ws_h']}, + {modules, ['gun','gun_app','gun_conns_sup','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_tunnel','gun_ws','gun_ws_h','gun_ws_protocol']}, {registered, [gun_sup]}, {applications, [kernel,stdlib,ssl,cowlib]}, {mod, {gun_app, []}}, diff --git a/src/gun.erl b/src/gun.erl index 880a2ae..b27ea6e 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -267,12 +267,14 @@ -type ws_opts() :: #{ closing_timeout => timeout(), compress => boolean(), + default_protocol => module(), flow => pos_integer(), keepalive => timeout(), protocols => [{binary(), module()}], reply_to => pid(), silence_pings => boolean(), - tunnel => stream_ref() + tunnel => stream_ref(), + user_opts => any() }. -export_type([ws_opts/0]). diff --git a/src/gun_ws_h.erl b/src/gun_ws_h.erl index 2412122..fca3c9a 100644 --- a/src/gun_ws_h.erl +++ b/src/gun_ws_h.erl @@ -13,6 +13,7 @@ %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(gun_ws_h). +-behavior(gun_ws_protocol). -export([init/4]). -export([handle/2]). diff --git a/src/gun_ws_protocol.erl b/src/gun_ws_protocol.erl new file mode 100644 index 0000000..7b0cc5c --- /dev/null +++ b/src/gun_ws_protocol.erl @@ -0,0 +1,25 @@ +%% Copyright (c) 2022, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_ws_protocol). + +-type protocol_state() :: any(). +-export_type([protocol_state/0]). + +-callback init(pid(), reference(), cow_http:headers(), gun:ws_opts()) + -> {ok, protocol_state()}. + +-callback handle(cow_ws:frame(), State) + -> {ok, non_neg_integer(), State} + when State::protocol_state(). -- cgit v1.2.3