diff options
-rw-r--r-- | doc/src/guide/connect.asciidoc | 24 | ||||
-rw-r--r-- | doc/src/manual/gun.asciidoc | 66 | ||||
-rw-r--r-- | src/gun.erl | 213 | ||||
-rw-r--r-- | src/gun_http.erl | 22 | ||||
-rw-r--r-- | src/gun_spdy.erl | 11 | ||||
-rw-r--r-- | test/ws_SUITE.erl | 2 |
6 files changed, 214 insertions, 124 deletions
diff --git a/doc/src/guide/connect.asciidoc b/doc/src/guide/connect.asciidoc index e1ad56e..e2bcaa7 100644 --- a/doc/src/guide/connect.asciidoc +++ b/doc/src/guide/connect.asciidoc @@ -33,16 +33,24 @@ The `gun:open/{2,3}` function must be used to open a connection. [source,erlang] {ok, ConnPid} = gun:open("example.org", 443). -@todo open/3 -@todo make opts a map +If the port given is 443, Gun will attempt to connect using +SSL. The protocol will be selected automatically using the +NPN extension for TLS. By default Gun supports SPDY/3.1, +SPDY/3 and HTTP/1.1 when connecting using SSL. -If the port given is 80, Gun will attempt to connect using -TCP and use the HTTP/1.1 protocol. For any other port, TLS -will be used. The NPN extension for TLS allows Gun to select -SPDY automatically if the server supports it. Otherwise, -HTTP/1.1 will be used. +For any other port, Gun will attempt to connect using TCP +and will use the HTTP/1.1 protocol. -@todo more about defaults +The transport and protocol used can be overriden using +options. The manual documents all available options. + +Options can be provided as a third argument, and take the +form of a map. + +.Opening an SSL connection to example.org on port 8443 + +[source,erlang] +{ok, ConnPid} = gun:open("example.org", 8443, #{transport=>ssl}). === Monitoring the connection process diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc index 14641ee..9d592a8 100644 --- a/doc/src/manual/gun.asciidoc +++ b/doc/src/manual/gun.asciidoc @@ -12,30 +12,62 @@ HTTP or Websocket. == Types -=== opts() = [opt()] +=== opts() = map() Configuration for the connection. -@todo Should be a map. - -With opt(): - -keepalive => pos_integer():: - Time between pings in milliseconds. - Defaults to 5000. +The following keys are defined: + +http_opts => gun:http_opts():: + Options specific to the HTTP protocol. See below. +protocols => [http | spdy]:: + Ordered list of preferred protocols. When the transport is tcp, + this list must contain exactly one protocol. When the transport + is ssl, this list must contain at least one protocol and will be + used using the NPN protocol negotiation method. When the server + does not support NPN then http will always be used. Defaults to + [http] when the transport is tcp, and [spdy, http] when the + transport is ssl. retry => non_neg_integer():: Number of times Gun will try to reconnect on failure before giving up. Defaults to 5. retry_timeout => pos_integer():: - Time between retries in milliseconds. - Defaults to 5000. -type => ssl | tcp | tcp_spdy:: - Whether to use SSL, plain TCP (for HTTP/Websocket) or SPDY over TCP. - The default varies depending on the port used. Port 443 defaults - to ssl. Port 6121 defaults to tcp_spdy (@todo). All other ports - default to tcp. (@todo) - -@todo We want to separate protocol and transport options. + Time between retries in milliseconds. Defaults to 5000. +spdy_opts => gun:spdy_opts():: + Options specific to the SPDY protocol. See below. +trace => boolean():: + Whether to enable `dbg` tracing of the connection process. Should + only be used during debugging. Defaults to false. +transport => tcp | ssl:: + Whether to use SSL or plain TCP. The default varies depending on the + port used. Port 443 defaults to ssl. All other ports default to tcp. +transport_opts => proplists:proplist():: + Transport options. They are TCP options or SSL options depending on + the selected transport. + +=== http_opts() = map() + +Configuration for the HTTP protocol. + +The following keys are defined: + +keepalive => pos_integer():: + Time between pings in milliseconds. Since the HTTP protocol has + no standardized way to ping the server, Gun will simply send an + empty line when the connection is idle. Gun only makes a best + effort here as servers usually have configurable limits to drop + idle connections. Defaults to 5000. +version => 'HTTP/1.1' | 'HTTP/1.0':: + HTTP version to use. Defaults to 'HTTP/1.1'. + +=== spdy_opts() = map() + +Configuration for the SPDY protocol. + +The following keys are defined: + +keepalive => pos_integer():: + Time between pings in milliseconds. Defaults to 5000. @todo We need to document Websocket options. diff --git a/src/gun.erl b/src/gun.erl index b7260bc..0fddfe3 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -71,7 +71,6 @@ -export([system_terminate/4]). -export([system_code_change/4]). --type conn_type() :: ssl | tcp | tcp_spdy. -type headers() :: [{binary(), iodata()}]. -type ws_close_code() :: 1000..4999. @@ -79,13 +78,15 @@ | {text | binary | close | ping | pong, iodata()} | {close, ws_close_code(), iodata()}. --type opts() :: [{http, gun_http:opts()} - | {keepalive, pos_integer()} - | {retry, non_neg_integer()} - | {retry_timeout, pos_integer()} - | {type, conn_type()}]. +-type opts() :: map(). -export_type([opts/0]). +-type http_opts() :: map(). +-export_type([http_opts/0]). + +-type spdy_opts() :: map(). +-export_type([spdy_opts/0]). + -type ws_opts() :: [{compress, boolean()}]. -record(state, { @@ -93,15 +94,11 @@ owner :: pid(), host :: inet:hostname(), port :: inet:port_number(), - keepalive :: pos_integer(), + opts :: opts(), keepalive_ref :: reference(), - type :: conn_type(), - retry :: non_neg_integer(), - retry_timeout :: pos_integer(), socket :: inet:socket() | ssl:sslsocket(), transport :: module(), protocol :: module(), - proto_opts :: gun_http:opts(), %% @todo Make a tuple with SPDY too. protocol_state :: any() }). @@ -110,35 +107,78 @@ -spec open(inet:hostname(), inet:port_number()) -> {ok, pid()} | {error, any()}. open(Host, Port) -> - open(Host, Port, []). + open(Host, Port, #{}). -spec open(inet:hostname(), inet:port_number(), opts()) -> {ok, pid()} | {error, any()}. open(Host, Port, Opts) when is_list(Host); is_atom(Host) -> - case open_opts(Opts) of + case check_options(maps:to_list(Opts)) of ok -> - supervisor:start_child(gun_sup, [self(), Host, Port, Opts]); - Error -> - Error + case supervisor:start_child(gun_sup, [self(), Host, Port, Opts]) of + OK = {ok, ServerPid} -> + consider_tracing(ServerPid, Opts), + OK; + StartError -> + StartError + end; + CheckError -> + CheckError end. -%% @private -open_opts([]) -> +check_options([]) -> ok; -open_opts([{http, O}|Opts]) when is_list(O) -> - open_opts(Opts); -open_opts([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> - open_opts(Opts); -open_opts([{retry, R}|Opts]) when is_integer(R), R >= 0 -> - open_opts(Opts); -open_opts([{retry_timeout, T}|Opts]) when is_integer(T) > 0 -> - open_opts(Opts); -open_opts([{type, T}|Opts]) - when T =:= tcp; T =:= tcp_spdy; T =:= ssl -> - open_opts(Opts); -open_opts([Opt|_]) -> +check_options([{http_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_http:check_options(map:to_list(ProtoOpts)) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([Opt = {protocols, L}|Opts]) when is_list(L) -> + Len = length(L), + case length(lists:usort(L)) of + Len when Len > 0 -> + Check = lists:usort([(P =:= http) orelse (P =:= spdy) || P <- L]), + case Check of + [true] -> + check_options(Opts); + _ -> + {error, {options, Opt}} + end; + _ -> + {error, {options, Opt}} + end; +check_options([{retry, R}|Opts]) when is_integer(R), R >= 0 -> + check_options(Opts); +check_options([{retry_timeout, T}|Opts]) when is_integer(T) > 0 -> + check_options(Opts); +check_options([{spdy_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_spdy:check_options(map:to_list(ProtoOpts)) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([{trace, B}|Opts]) when B =:= true; B =:= false -> + check_options(Opts); +check_options([{transport, T}|Opts]) when T =:= tcp; T =:= ssl -> + check_options(Opts); +check_options([{transport_opts, L}|Opts]) when is_list(L) -> + check_options(Opts); +check_options([Opt|_]) -> {error, {options, Opt}}. +consider_tracing(ServerPid, #{trace := true}) -> + dbg:start(), + dbg:tracer(), + dbg:tpl(gun, [{'_', [], [{return_trace}]}]), + dbg:tpl(gun_http, [{'_', [], [{return_trace}]}]), + dbg:tpl(gun_spdy, [{'_', [], [{return_trace}]}]), + dbg:tpl(gun_ws, [{'_', [], [{return_trace}]}]), + dbg:p(ServerPid, all); +consider_tracing(_, _) -> + ok. + -spec close(pid()) -> ok. close(ServerPid) -> supervisor:terminate_child(gun_sup, ServerPid). @@ -372,62 +412,49 @@ start_link(Owner, Host, Port, Opts) -> proc_lib:start_link(?MODULE, init, [self(), Owner, Host, Port, Opts]). -%% @doc Faster alternative to proplists:get_value/3. -%% @private -get_value(Key, Opts, Default) -> - case lists:keyfind(Key, 1, Opts) of - {_, Value} -> Value; - _ -> Default - end. - init(Parent, Owner, Host, Port, Opts) -> ok = proc_lib:init_ack(Parent, {ok, self()}), - HTTPOpts = get_value(http, Opts, []), - Keepalive = get_value(keepalive, Opts, 5000), - Retry = get_value(retry, Opts, 5), - RetryTimeout = get_value(retry_timeout, Opts, 5000), - %% Default to TCP if port 80 is given, otherwise SSL. - Type = get_value(type, Opts, if Port =:= 80 -> tcp; true -> ssl end), - connect(#state{parent=Parent, owner=Owner, host=Host, port=Port, - keepalive=Keepalive, type=Type, retry=Retry, - proto_opts=HTTPOpts, retry_timeout=RetryTimeout}, Retry). - -connect(State=#state{owner=Owner, host=Host, port=Port, type=ssl, - proto_opts=HTTPOpts}, Retries) -> - Transport = ranch_ssl, - %% R15 support. - HasNPN = erlang:function_exported(ssl, negotiated_next_protocol, 1), - Opts = [binary, {active, false} - |[{client_preferred_next_protocols, - {client, [<<"spdy/3">>, <<"http/1.1">>], <<"http/1.1">>}} - || HasNPN]], - case Transport:connect(Host, Port, Opts) of + Retry = maps:get(retry, Opts, 5), + Transport = case maps:get(transport, Opts, default_transport(Port)) of + tcp -> ranch_tcp; + ssl -> ranch_ssl + end, + connect(#state{parent=Parent, owner=Owner, host=Host, port=Port, opts=Opts, transport=Transport}, Retry). + +default_transport(443) -> ssl; +default_transport(_) -> tcp. + +connect(State=#state{owner=Owner, host=Host, port=Port, opts=Opts, transport=Transport=ranch_ssl}, Retries) -> + Protocols = lists:flatten([case P of + http -> <<"http/1.1">>; + spdy -> [<<"spdy/3.1">>, <<"spdy/3">>] + end || P <- maps:get(protocols, Opts, [spdy, http])]), + TransportOpts = [binary, {active, false}, + {client_preferred_next_protocols, {client, Protocols, <<"http/1.1">>}} + |maps:get(transport_opts, Opts, [])], + case Transport:connect(Host, Port, TransportOpts) of {ok, Socket} -> - {Protocol, ProtoOpts} = case HasNPN of - false -> - {gun_http, HTTPOpts}; - true -> - case ssl:negotiated_next_protocol(Socket) of - {ok, <<"spdy/3">>} -> {gun_spdy, []}; - _ -> {gun_http, HTTPOpts} - end + {Protocol, ProtoOptsKey} = case ssl:negotiated_next_protocol(Socket) of + {ok, <<"spdy/3", _/bits>>} -> {gun_spdy, spdy_opts}; + _ -> {gun_http, http_opts} end, + ProtoOpts = maps:get(ProtoOptsKey, Opts, #{}), ProtoState = Protocol:init(Owner, Socket, Transport, ProtoOpts), before_loop(State#state{socket=Socket, transport=Transport, protocol=Protocol, protocol_state=ProtoState}); {error, _} -> retry(State, Retries - 1) end; -connect(State=#state{owner=Owner, host=Host, port=Port, type=Type, - proto_opts=HTTPOpts}, Retries) -> - Transport = ranch_tcp, - Opts = [binary, {active, false}], - case Transport:connect(Host, Port, Opts) of +connect(State=#state{owner=Owner, host=Host, port=Port, opts=Opts, transport=Transport}, Retries) -> + TransportOpts = [binary, {active, false} + |maps:get(transport_opts, Opts, [])], + case Transport:connect(Host, Port, TransportOpts) of {ok, Socket} -> - {Protocol, ProtoOpts} = case Type of - tcp_spdy -> {gun_spdy, []}; - tcp -> {gun_http, HTTPOpts} + {Protocol, ProtoOptsKey} = case maps:get(protocols, Opts, [http]) of + [http] -> {gun_http, http_opts}; + [spdy] -> {gun_spdy, spdy_opts} end, + ProtoOpts = maps:get(ProtoOptsKey, Opts, #{}), ProtoState = Protocol:init(Owner, Socket, Transport, ProtoOpts), before_loop(State#state{socket=Socket, transport=Transport, protocol=Protocol, protocol_state=ProtoState}); @@ -435,6 +462,9 @@ connect(State=#state{owner=Owner, host=Host, port=Port, type=Type, retry(State, Retries - 1) end. +retry(State=#state{opts=Opts}) -> + retry(State, maps:get(retry, Opts, 5)). + %% Exit normally if the retry functionality has been disabled. retry(_, 0) -> ok; @@ -453,8 +483,8 @@ retry(State, Retries) -> %% Too many retries, give up. retry_loop(_, 0) -> error(gone); -retry_loop(State=#state{parent=Parent, retry_timeout=RetryTimeout}, Retries) -> - _ = erlang:send_after(RetryTimeout, self(), retry), +retry_loop(State=#state{parent=Parent, opts=Opts}, Retries) -> + _ = erlang:send_after(maps:get(retry_timeout, Opts, 5000), self(), retry), receive retry -> connect(State, Retries); @@ -463,13 +493,18 @@ retry_loop(State=#state{parent=Parent, retry_timeout=RetryTimeout}, Retries) -> {retry_loop, State, Retries}) end. -before_loop(State=#state{keepalive=Keepalive}) -> +before_loop(State=#state{opts=Opts, protocol=Protocol}) -> + ProtoOptsKey = case Protocol of + gun_http -> http_opts; + gun_spdy -> spdy_opts + end, + ProtoOpts = maps:get(ProtoOptsKey, Opts, #{}), + Keepalive = maps:get(keepalive, ProtoOpts, 5000), KeepaliveRef = erlang:send_after(Keepalive, self(), keepalive), loop(State#state{keepalive_ref=KeepaliveRef}). loop(State=#state{parent=Parent, owner=Owner, host=Host, port=Port, - retry=Retry, socket=Socket, transport=Transport, - protocol=Protocol, protocol_state=ProtoState}) -> + socket=Socket, transport=Transport, protocol=Protocol, protocol_state=ProtoState}) -> {OK, Closed, Error} = Transport:messages(), Transport:setopts(Socket, [{active, once}]), receive @@ -478,7 +513,7 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host, port=Port, close -> Transport:close(Socket), retry(State#state{socket=undefined, transport=undefined, - protocol=undefined}, Retry); + protocol=undefined}); {upgrade, Protocol2, ProtoState2} -> ws_loop(State#state{protocol=Protocol2, protocol_state=ProtoState2}); ProtoState2 -> @@ -488,12 +523,12 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host, port=Port, Protocol:close(ProtoState), Transport:close(Socket), retry(State#state{socket=undefined, transport=undefined, - protocol=undefined}, Retry); + protocol=undefined}); {Error, Socket, _} -> Protocol:close(ProtoState), Transport:close(Socket), retry(State#state{socket=undefined, transport=undefined, - protocol=undefined}, Retry); + protocol=undefined}); {OK, _PreviousSocket, _Data} -> loop(State); {Closed, _PreviousSocket} -> @@ -518,8 +553,8 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host, port=Port, {cancel, Owner, StreamRef} -> ProtoState2 = Protocol:cancel(ProtoState, StreamRef), loop(State#state{protocol_state=ProtoState2}); - {ws_upgrade, Owner, StreamRef, Path, Headers, Opts} when Protocol =/= gun_spdy -> - ProtoState2 = Protocol:ws_upgrade(ProtoState, StreamRef, Host, Port, Path, Headers, Opts), + {ws_upgrade, Owner, StreamRef, Path, Headers, WsOpts} when Protocol =/= gun_spdy -> + ProtoState2 = Protocol:ws_upgrade(ProtoState, StreamRef, Host, Port, Path, Headers, WsOpts), loop(State#state{protocol_state=ProtoState2}); %% @todo can fail if http/1.0 {shutdown, Owner} -> @@ -549,7 +584,7 @@ loop(State=#state{parent=Parent, owner=Owner, host=Host, port=Port, loop(State) end. -ws_loop(State=#state{parent=Parent, owner=Owner, retry=Retry, socket=Socket, +ws_loop(State=#state{parent=Parent, owner=Owner, socket=Socket, transport=Transport, protocol=Protocol, protocol_state=ProtoState}) -> {OK, Closed, Error} = Transport:messages(), ok = Transport:setopts(Socket, [{active, once}]), @@ -558,16 +593,16 @@ ws_loop(State=#state{parent=Parent, owner=Owner, retry=Retry, socket=Socket, case Protocol:handle(Data, ProtoState) of close -> Transport:close(Socket), - retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry); + retry(State#state{socket=undefined, transport=undefined, protocol=undefined}); ProtoState2 -> ws_loop(State#state{protocol_state=ProtoState2}) end; {Closed, Socket} -> Transport:close(Socket), - retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry); + retry(State#state{socket=undefined, transport=undefined, protocol=undefined}); {Error, Socket, _} -> Transport:close(Socket), - retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry); + retry(State#state{socket=undefined, transport=undefined, protocol=undefined}); %% Ignore any previous HTTP keep-alive. keepalive -> ws_loop(State); @@ -577,7 +612,7 @@ ws_loop(State=#state{parent=Parent, owner=Owner, retry=Retry, socket=Socket, case Protocol:send(Frame, ProtoState) of close -> Transport:close(Socket), - retry(State#state{socket=undefined, transport=undefined, protocol=undefined}, Retry); + retry(State#state{socket=undefined, transport=undefined, protocol=undefined}); ProtoState2 -> ws_loop(State#state{protocol_state=ProtoState2}) end; diff --git a/src/gun_http.erl b/src/gun_http.erl index 745c2a9..4726f03 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -14,6 +14,7 @@ -module(gun_http). +-export([check_options/1]). -export([init/4]). -export([handle/2]). -export([close/1]). @@ -24,9 +25,6 @@ -export([cancel/2]). -export([ws_upgrade/7]). --type opts() :: [{version, cow_http:version()}]. --export_type([opts/0]). - -type io() :: head | {body, non_neg_integer()} | body_close | body_chunked. -type websocket_info() :: {websocket, reference(), binary(), [], []}. %% key, extensions, protocols @@ -44,11 +42,19 @@ out = head :: io() }). -init(Owner, Socket, Transport, []) -> - #http_state{owner=Owner, socket=Socket, transport=Transport}; -init(Owner, Socket, Transport, [{version, Version}]) -> - #http_state{owner=Owner, socket=Socket, transport=Transport, - version=Version}. +check_options(Opts) -> + do_check_options(map:to_list(Opts)). + +do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> + do_check_options(Opts); +do_check_options([{version, V}|Opts]) when V =:= 'HTTP/1.1'; V =:= 'HTTP/1.0' -> + do_check_options(Opts); +do_check_options([Opt|_]) -> + {error, {options, {http, Opt}}}. + +init(Owner, Socket, Transport, Opts) -> + Version = maps:get(version, Opts, 'HTTP/1.1'), + #http_state{owner=Owner, socket=Socket, transport=Transport, version=Version}. %% Stop looping when we got no more data. handle(<<>>, State) -> diff --git a/src/gun_spdy.erl b/src/gun_spdy.erl index 7651584..ee61659 100644 --- a/src/gun_spdy.erl +++ b/src/gun_spdy.erl @@ -14,6 +14,7 @@ -module(gun_spdy). +-export([check_options/1]). -export([init/4]). -export([handle/2]). -export([close/1]). @@ -43,7 +44,15 @@ ping_id = 1 :: non_neg_integer() }). -init(Owner, Socket, Transport, []) -> +check_options(Opts) -> + do_check_options(map:to_list(Opts)). + +do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> + do_check_options(Opts); +do_check_options([Opt|_]) -> + {error, {options, {spdy, Opt}}}. + +init(Owner, Socket, Transport, _Opts) -> #spdy_state{owner=Owner, socket=Socket, transport=Transport, zdef=cow_spdy:deflate_init(), zinf=cow_spdy:inflate_init()}. diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl index 82f3632..2310b31 100644 --- a/test/ws_SUITE.erl +++ b/test/ws_SUITE.erl @@ -140,7 +140,7 @@ log_output() -> ok. connect(Path) -> - {ok, Pid} = gun:open("127.0.0.1", 33080, [{type, tcp}, {retry, 0}]), + {ok, Pid} = gun:open("127.0.0.1", 33080, #{retry=>0}), Ref = monitor(process, Pid), gun:ws_upgrade(Pid, Path, [], #{compress => true}), receive |