diff options
-rw-r--r-- | doc/src/manual/gun.asciidoc | 111 | ||||
-rw-r--r-- | doc/src/manual/gun.await_up.asciidoc | 3 | ||||
-rw-r--r-- | doc/src/manual/gun.info.asciidoc | 6 | ||||
-rw-r--r-- | doc/src/manual/gun.open.asciidoc | 5 | ||||
-rw-r--r-- | doc/src/manual/gun.open_unix.asciidoc | 5 | ||||
-rw-r--r-- | doc/src/manual/gun_down.asciidoc | 3 | ||||
-rw-r--r-- | doc/src/manual/gun_error.asciidoc | 1 | ||||
-rw-r--r-- | doc/src/manual/gun_socks_up.asciidoc | 66 | ||||
-rw-r--r-- | doc/src/manual/gun_up.asciidoc | 13 | ||||
-rw-r--r-- | src/gun.erl | 31 | ||||
-rw-r--r-- | src/gun_event.erl | 6 | ||||
-rw-r--r-- | src/gun_http.erl | 4 | ||||
-rw-r--r-- | test/event_SUITE.erl | 4 | ||||
-rw-r--r-- | test/socks_SUITE.erl | 6 |
14 files changed, 216 insertions, 48 deletions
diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc index b9fbbc2..7b54666 100644 --- a/doc/src/manual/gun.asciidoc +++ b/doc/src/manual/gun.asciidoc @@ -63,6 +63,7 @@ by sending any of the following messages: Connection: * link:man:gun_up(3)[gun_up(3)] - The connection is up +* link:man:gun_socks_up(3)[gun_socks_up(3)] - The Socks connection is up * link:man:gun_down(3)[gun_down(3)] - The connection is down * link:man:gun_upgrade(3)[gun_upgrade(3)] - Successful protocol upgrade * link:man:gun_error(3)[gun_error(3)] - Stream or connection-wide error @@ -96,7 +97,7 @@ connect_destination() :: #{ username => iodata(), password => iodata(), - protocols => [http | http2], + protocols => protocols(), transport => tcp | tls, tls_opts => [ssl:tls_client_option()], @@ -121,15 +122,17 @@ username, password:: Proxy authorization credentials. They are only sent when both options are provided. -protocol (http):: +protocols - see below:: -Protocol that will be used for tunneled requests. +Ordered list of preferred protocols. Please refer to the +`protocols()` type documentation for details. ++ +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. transport (tcp):: -Transport that will be used for tunneled requests. Note that -due to Erlang/OTP limitations it is not possible to tunnel -a TLS connection inside a TLS tunnel. +Transport that will be used for tunneled requests. tls_opts ([]):: @@ -233,7 +236,7 @@ opts() :: #{ domain_lookup_timeout => timeout(), http_opts => http_opts(), http2_opts => http2_opts(), - protocols => [http | http2], + protocols => protocols(), retry => non_neg_integer(), retry_fun => fun(), retry_timeout => pos_integer(), @@ -269,13 +272,11 @@ Options specific to the HTTP/2 protocol. protocols - see below:: -Ordered list of preferred protocols. When the transport is `tcp`, -this list must contain exactly one protocol. When the transport -is `tls`, this list must contain at least one protocol and will be -used to negotiate a protocol via ALPN. When the server does not -support ALPN then `http` will always be used. Defaults to -`[http]` when the transport is `tcp`, and `[http2, http]` when the -transport is `tls`. +Ordered list of preferred protocols. Please refer to the +`protocols()` type documentation for details. ++ +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. retry (5):: @@ -340,6 +341,25 @@ ws_opts (#{}):: Options specific to the Websocket protocol. +=== protocols() + +[source,erlang] +---- +protocols() :: http | {http, http_opts()} + | http2 | {http2, http2_opts()} + | socks | {socks, socks_opts()} +---- + +Ordered list of preferred protocols. When the transport is `tcp`, +this list must contain exactly one protocol. When the transport +is `tls`, this list must contain at least one protocol and will be +used to negotiate a protocol via ALPN. When the server does not +support ALPN then `http` will be used, except when the list of +preferred protocols is set to only accept `socks`. + +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. + === req_headers() [source,erlang] @@ -373,6 +393,64 @@ reply_to (`self()`):: The pid of the process that will receive the response messages. +=== socks_opts() + +[source,erlang] +---- +socks_opts() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + + auth => [{username_password, binary(), binary()} | none], + protocols => protocols(), + transport => tcp | tls, + version => 5, + + tls_opts => [ssl:tls_client_option()], + tls_handshake_timeout => timeout() +} +---- + +Configuration for the Socks protocol. + +The default value, if any, is given next to the option name: + +host, port:: + +Destination hostname and port number. Mandatory. ++ +Upon successful completion of the Socks connection, Gun will +begin using these as the host and port of the origin server +for subsequent requests. + +auth ([none]):: + +Authentication methods Gun advertises to the Socks proxy. + +protocols - see below:: + +Ordered list of preferred protocols. Please refer to the +`protocols()` type documentation for details. ++ +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. + +transport (tcp):: + +Transport that will be used for tunneled requests. + +tls_opts ([]):: + +Options to use for tunneled TLS connections. + +tls_handshake_timeout (infinity):: + +Handshake timeout for tunneled TLS connections. + +version (5):: + +Version of the Socks protocol to use. + === ws_opts() [source,erlang] @@ -417,6 +495,11 @@ undocumented and must be set to `gun_ws_h`. == Changelog +* *2.0*: The types `protocols()` and `socks_opts()` have been + added. Support for the Socks protocol has been added + in every places where protocol selection is available. + In addition it is now possible to specify separate + HTTP options for the CONNECT proxy and the origin server. * *2.0*: The `connect_timeout` option has been split into three options: `domain_lookup_timeout`, `connect_timeout` and when applicable `tls_handshake_timeout`. diff --git a/doc/src/manual/gun.await_up.asciidoc b/doc/src/manual/gun.await_up.asciidoc index dfd4da9..d9934b5 100644 --- a/doc/src/manual/gun.await_up.asciidoc +++ b/doc/src/manual/gun.await_up.asciidoc @@ -23,7 +23,7 @@ await_up(ConnPid, Timeout, MonitorRef) ConnPid :: pid() MonitorRef :: reference() Timeout :: timeout() -Protocol :: http | http2 +Protocol :: http | http2 | socks Reason :: {down, any()} | timeout ---- @@ -71,4 +71,5 @@ may also be returned when a timeout or an error occur. link:man:gun(3)[gun(3)], link:man:gun:open(3)[gun:open(3)], link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun_socks_up(3)[gun_socks_up(3)], link:man:gun_up(3)[gun_up(3)] diff --git a/doc/src/manual/gun.info.asciidoc b/doc/src/manual/gun.info.asciidoc index afb2333..cf861c9 100644 --- a/doc/src/manual/gun.info.asciidoc +++ b/doc/src/manual/gun.info.asciidoc @@ -14,7 +14,7 @@ ConnPid :: pid() Info :: #{ socket => inet:socket() | ssl:sslsocket(), transport => tcp | tls, - protocol => http | http2 | ws, + protocol => http | http2 | socks | ws, sock_ip => inet:ip_address(), sock_port => inet:port_number(), origin_host => inet:hostname() | inet:ip_address(), @@ -22,11 +22,11 @@ Info :: #{ intermediaries => [Intermediary] } Intermediary :: #{ - type => connect, + type => connect | socks5, host => inet:hostname() | inet:ip_address(), port => inet:port_number(), transport => tcp | tls, - protocol => http | http2 + protocol => http | http2 | socks } ---- diff --git a/doc/src/manual/gun.open.asciidoc b/doc/src/manual/gun.open.asciidoc index 79a9d54..63bb3f8 100644 --- a/doc/src/manual/gun.open.asciidoc +++ b/doc/src/manual/gun.open.asciidoc @@ -14,7 +14,9 @@ open(Host, Port, Opts) -> {ok, pid()} | {error, Reason} Host :: inet:hostname() | inet:ip_address() Port :: inet:port_number() Opts :: gun:opts() -Reason :: {options, OptName} | {options, {http | http2, OptName}} | any() +Reason :: {options, OptName} + | {options, {http | http2 | socks | ws, OptName}} + | any() OptName :: atom() ---- @@ -71,4 +73,5 @@ message will be sent for that. link:man:gun(3)[gun(3)], link:man:gun:open_unix(3)[gun:open_unix(3)], link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_socks_up(3)[gun_socks_up(3)], link:man:gun_up(3)[gun_up(3)] diff --git a/doc/src/manual/gun.open_unix.asciidoc b/doc/src/manual/gun.open_unix.asciidoc index 4ea8d2b..441bfa8 100644 --- a/doc/src/manual/gun.open_unix.asciidoc +++ b/doc/src/manual/gun.open_unix.asciidoc @@ -12,7 +12,9 @@ open_unix(SocketPath, Opts) -> {ok, pid()} | {error, Reason} SocketPath :: string() Opts :: gun:opts() -Reason :: {options, OptName} | {options, {http | http2, OptName}} | any() +Reason :: {options, OptName} + | {options, {http | http2 | socks | ws, OptName}} + | any() OptName :: atom() ---- @@ -59,4 +61,5 @@ message will be sent for that. link:man:gun(3)[gun(3)], link:man:gun:open(3)[gun:open(3)], link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_socks_up(3)[gun_socks_up(3)], link:man:gun_up(3)[gun_up(3)] diff --git a/doc/src/manual/gun_down.asciidoc b/doc/src/manual/gun_down.asciidoc index 67c7796..6c72898 100644 --- a/doc/src/manual/gun_down.asciidoc +++ b/doc/src/manual/gun_down.asciidoc @@ -11,7 +11,7 @@ gun_down - The connection is down {gun_down, ConnPid, Protocol, Reason, KilledStreams, UnprocessedStreams} ConnPid :: pid() -Protocol :: http | http2 | ws +Protocol :: http | http2 | socks | ws Reason :: any() KilledStreams :: [reference()] UnprocessedStreams :: [reference()] @@ -83,4 +83,5 @@ link:man:gun(3)[gun(3)], link:man:gun:open(3)[gun:open(3)], link:man:gun:open_unix(3)[gun:open_unix(3)], link:man:gun_up(3)[gun_up(3)], +link:man:gun_socks_up(3)[gun_socks_up(3)], link:man:gun_error(3)[gun_error(3)] diff --git a/doc/src/manual/gun_error.asciidoc b/doc/src/manual/gun_error.asciidoc index a43d32b..ac278fd 100644 --- a/doc/src/manual/gun_error.asciidoc +++ b/doc/src/manual/gun_error.asciidoc @@ -64,4 +64,5 @@ handle_info({gun_error, ConnPid, _StreamRef, _Reason}, link:man:gun(3)[gun(3)], link:man:gun_up(3)[gun_up(3)], +link:man:gun_socks_up(3)[gun_socks_up(3)], link:man:gun_down(3)[gun_down(3)] diff --git a/doc/src/manual/gun_socks_up.asciidoc b/doc/src/manual/gun_socks_up.asciidoc new file mode 100644 index 0000000..e74f1a9 --- /dev/null +++ b/doc/src/manual/gun_socks_up.asciidoc @@ -0,0 +1,66 @@ += gun_socks_up(3) + +== Name + +gun_socks_up - The Socks connection is up + +== Description + +[source,erlang] +---- +{gun_socks_up, ConnPid, Protocol} + +ConnPid :: pid() +Protocol :: http | http2 | socks +---- + +The Socks connection is up. + +This message informs the owner process that the connection +completed through the configured Socks proxy. + +If Gun is configured to connect to another Socks server, then the +connection is not usable yet. One or more +link:man:gun_socks_up(3)[gun_socks_up(3)] messages will follow. + +Otherwise, Gun will start processing the messages it received while +waiting for the connection to be up. If this is a reconnection, +then this may not be desirable for all requests. Those requests +should be cancelled when the connection goes down, and any +subsequent messages ignored. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +Protocol:: + +The protocol selected for this connection. It can be used +to determine the capabilities of the server. + +== Changelog + +* *2.0*: Message introduced. + +== Examples + +.Receive a gun_socks_up message in a gen_server +[source,erlang] +---- +handle_info({gun_socks_up, ConnPid, _Protocol}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_up(3)[gun_up(3)], +link:man:gun_down(3)[gun_down(3)], +link:man:gun_error(3)[gun_error(3)] diff --git a/doc/src/manual/gun_up.asciidoc b/doc/src/manual/gun_up.asciidoc index a103594..db09fca 100644 --- a/doc/src/manual/gun_up.asciidoc +++ b/doc/src/manual/gun_up.asciidoc @@ -11,7 +11,7 @@ gun_up - The connection is up {gun_up, ConnPid, Protocol} ConnPid :: pid() -Protocol :: http | http2 +Protocol :: http | http2 | socks ---- The connection is up. @@ -19,16 +19,16 @@ The connection is up. This message informs the owner process that the connection or reconnection completed. -Gun will now start processing the messages it received while +If Gun is configured to connect to a Socks server, then the +connection is not usable yet. One or more +link:man:gun_socks_up(3)[gun_socks_up(3)] messages will follow. + +Otherwise, Gun will start processing the messages it received while waiting for the connection to be up. If this is a reconnection, then this may not be desirable for all requests. Those requests should be cancelled when the connection goes down, and any subsequent messages ignored. -// @todo Gun doesn't process messages immediately if it -// is using the socks protocol, there are gun_socks_connected -// messages coming up before reaching HTTP. - == Elements ConnPid:: @@ -61,5 +61,6 @@ link:man:gun(3)[gun(3)], link:man:gun:open(3)[gun:open(3)], link:man:gun:open_unix(3)[gun:open_unix(3)], link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_socks_up(3)[gun_socks_up(3)], link:man:gun_down(3)[gun_down(3)], link:man:gun_error(3)[gun_error(3)] diff --git a/src/gun.erl b/src/gun.erl index e576b61..6efca0b 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -114,19 +114,22 @@ | {close, ws_close_code(), iodata()}. -export_type([ws_frame/0]). +-type protocols() :: [http | http2 | socks + | {http, http_opts()} | {http2, http2_opts()} | {socks, socks_opts()}]. +-export_type([protocols/0]). + -type opts() :: #{ connect_timeout => timeout(), domain_lookup_timeout => timeout(), event_handler => {module(), any()}, http_opts => http_opts(), http2_opts => http2_opts(), - protocols => [http | http2 | {socks, socks_opts()}], + protocols => protocols(), retry => non_neg_integer(), retry_fun => fun((non_neg_integer(), opts()) -> #{retries => non_neg_integer(), timeout => pos_integer()}), retry_timeout => pos_integer(), - %% @todo Not sure this should be allowed, there could be loops. -% socks_opts => socks_opts(), + socks_opts => socks_opts(), supervise => boolean(), tcp_opts => [gen_tcp:connect_option()], tls_handshake_timeout => timeout(), @@ -144,9 +147,7 @@ username => iodata(), password => iodata(), protocol => http | http2, %% @todo Remove in Gun 2.0. - %% @todo It could be interesting to accept {http, http_opts()} - %% as well since we may want different options for proxy and origin. - protocols => [http | http2 | {socks, socks_opts()}], + protocols => protocols(), transport => tcp | tls, tls_opts => [ssl:tls_client_option()], tls_handshake_timeout => timeout() @@ -192,7 +193,7 @@ auth => [{username_password, binary(), binary()} | none], host := inet:hostname() | inet:ip_address(), port := inet:port_number(), - protocols => [http | http2 | {socks, socks_opts()}], + protocols => protocols(), transport => tcp | tls, tls_opts => [ssl:tls_client_option()], tls_handshake_timeout => timeout() @@ -306,6 +307,13 @@ check_options([{retry_fun, F}|Opts]) when is_function(F, 2) -> check_options(Opts); check_options([{retry_timeout, T}|Opts]) when is_integer(T), T >= 0 -> check_options(Opts); +check_options([{socks_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_socks:check_options(ProtoOpts) of + ok -> + check_options(Opts); + Error -> + Error + end; check_options([{supervise, B}|Opts]) when B =:= true; B =:= false -> check_options(Opts); check_options([{tcp_opts, L}|Opts]) when is_list(L) -> @@ -340,8 +348,9 @@ check_protocols_opt(Protocols) -> true -> %% When options are given alongside a protocol, they %% must be checked as well. - %% @todo It may be interesting to allow more than just socks here. TupleCheck = [case P of + {http, Opts} -> gun_http:check_options(Opts); + {http2, Opts} -> gun_http2:check_options(Opts); {socks, Opts} -> gun_socks:check_options(Opts) end || P <- Protocols, is_tuple(P)], case lists:usort(TupleCheck) of @@ -683,7 +692,7 @@ await_up(ServerPid, Timeout, MRef) -> receive {gun_up, ServerPid, Protocol} -> {ok, Protocol}; - {gun_socks_connected, ServerPid, Protocol} -> + {gun_socks_up, ServerPid, Protocol} -> {ok, Protocol}; {'DOWN', MRef, process, ServerPid, Reason} -> {error, {down, Reason}} @@ -1289,9 +1298,9 @@ commands([{switch_protocol, Protocol0}], State0=#state{ Protocol1 = protocol_handler(P), {Protocol1, maps:get(Protocol1:opts_name(), Opts, #{})} end, - %% When we switch_protocol from socks we must send a gun_socks_connected message. + %% When we switch_protocol from socks we must send a gun_socks_up message. _ = case CurrentProtocol of - gun_socks -> Owner ! {gun_socks_connected, self(), Protocol:name()}; + gun_socks -> Owner ! {gun_socks_up, self(), Protocol:name()}; _ -> ok end, {StateName, ProtoState} = Protocol:init(Owner, Socket, Transport, ProtoOpts), diff --git a/src/gun_event.erl b/src/gun_event.erl index a984796..0c1326d 100644 --- a/src/gun_event.erl +++ b/src/gun_event.erl @@ -47,7 +47,7 @@ lookup_info := gun_tcp:lookup_info(), timeout := timeout(), socket => inet:socket(), - protocol => http | http2, %% Only when transport is tcp. + protocol => http | http2 | socks, %% Only when transport is tcp. error => any() }. @@ -67,7 +67,7 @@ socket := inet:socket() | ssl:sslsocket() | pid(), %% The socket before/after will be different. tls_opts := [ssl:tls_client_option()], timeout := timeout(), - protocol => http | http2, + protocol => http | http2 | socks, error => any() }. @@ -241,7 +241,7 @@ %% support CONNECT and Websocket over HTTP/2. -type protocol_changed_event() :: #{ - protocol := http2 | ws + protocol := http | http2 | socks | ws }. -callback protocol_changed(protocol_changed_event(), State) -> State. diff --git a/src/gun_http.erl b/src/gun_http.erl index ce174ce..8860baa 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -301,7 +301,6 @@ handle_head(Data, State=#http_state{version=ClientVersion, content_handlers=Hand _ = end_stream(State#http_state{streams=[Stream|Tail]}), NewHost = maps:get(host, Destination), NewPort = maps:get(port, Destination), - Protocols = maps:get(protocols, Destination, [http]), case Destination of #{transport := tls} -> HandshakeEvent = #{ @@ -310,10 +309,11 @@ handle_head(Data, State=#http_state{version=ClientVersion, content_handlers=Hand tls_opts => maps:get(tls_opts, Destination, []), timeout => maps:get(tls_handshake_timeout, Destination, infinity) }, + Protocols = maps:get(protocols, Destination, [http2, http]), {[{origin, <<"https">>, NewHost, NewPort, connect}, {tls_handshake, HandshakeEvent, Protocols}], EvHandlerState1}; _ -> - [Protocol] = Protocols, + [Protocol] = maps:get(protocols, Destination, [http]), {[{origin, <<"http">>, NewHost, NewPort, connect}, {switch_protocol, Protocol}], EvHandlerState1} end; diff --git a/test/event_SUITE.erl b/test/event_SUITE.erl index 977eec4..5510e8c 100644 --- a/test/event_SUITE.erl +++ b/test/event_SUITE.erl @@ -315,7 +315,7 @@ http1_tls_handshake_end_ok_connect(Config) -> socket := Socket, tls_opts := _, timeout := _, - protocol := http + protocol := http2 } = do_receive_event(tls_handshake_end), true = is_tuple(Socket), gun:close(ConnPid). @@ -405,7 +405,7 @@ http1_tls_handshake_end_ok_connect_over_https_proxy(Config) -> socket := Socket, tls_opts := _, timeout := _, - protocol := http + protocol := http2 } = do_receive_event(tls_handshake_end), true = is_pid(Socket), gun:close(ConnPid). diff --git a/test/socks_SUITE.erl b/test/socks_SUITE.erl index 060c89f..ec184a6 100644 --- a/test/socks_SUITE.erl +++ b/test/socks_SUITE.erl @@ -222,7 +222,7 @@ do_socks5(OriginScheme, OriginTransport, OriginProtocol, ProxyTransport, SocksAu protocols => [OriginProtocol] }}] }), - %% We receive a gun_up and a gun_socks_connected. + %% We receive a gun_up and a gun_socks_up. {ok, socks} = gun:await_up(ConnPid), {ok, OriginProtocol} = gun:await_up(ConnPid), %% The proxy received two packets. @@ -300,7 +300,7 @@ do_socks5_through_multiple_proxies(OriginScheme, OriginTransport, ProxyTransport }}] }}] }), - %% We receive a gun_up and two gun_socks_connected. + %% We receive a gun_up and two gun_socks_up. {ok, socks} = gun:await_up(ConnPid), {ok, socks} = gun:await_up(ConnPid), {ok, http} = gun:await_up(ConnPid), @@ -386,7 +386,7 @@ do_socks5_through_connect_proxy(OriginScheme, OriginTransport, ProxyTransport) - }), {request, <<"CONNECT">>, Authority1, 'HTTP/1.1', _} = receive_from(Proxy1Pid), {response, fin, 200, _} = gun:await(ConnPid, StreamRef), - %% We receive a gun_socks_connected afterwards. This is the origin HTTP server. + %% We receive a gun_socks_up afterwards. This is the origin HTTP server. {ok, http} = gun:await_up(ConnPid), %% The second proxy receives a Socks5 auth/connect request. {auth_methods, 1, [none]} = receive_from(Proxy2Pid), |