%% Copyright (c) 2019-2023, 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(raw_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [doc/1]). -import(gun_test, [init_origin/3]). -import(gun_test, [receive_from/1]). all() -> [{group, raw}]. groups() -> [{raw, [parallel], ct_helper:all(?MODULE)}]. %% Tests. direct_raw_tcp(_) -> doc("Directly connect to a remote endpoint using the raw protocol over TCP."), do_direct_raw(tcp, flow_control_disabled, client_side_close). direct_raw_tls(_) -> doc("Directly connect to a remote endpoint using the raw protocol over TLS."), do_direct_raw(tls, flow_control_disabled, client_side_close). direct_raw_tcp_with_flow_control(_) -> doc("Directly connect to a remote endpoint using the raw protocol over TCP " "with flow control enabled."), do_direct_raw(tcp, flow_control_enabled, client_side_close). direct_raw_tls_with_flow_control(_) -> doc("Directly connect to a remote endpoint using the raw protocol over TLS " "with flow control enabled."), do_direct_raw(tls, flow_control_enabled, client_side_close). direct_raw_tcp_with_server_side_close(_) -> doc("Directly connect to a remote endpoint using the raw protocol over TCP " "with server-side close."), do_direct_raw(tcp, flow_control_disabled, server_side_close). direct_raw_tls_with_server_side_close(_) -> doc("Directly connect to a remote endpoint using the raw protocol over TLS " "with server-side close."), do_direct_raw(tls, flow_control_disabled, server_side_close). do_direct_raw(OriginTransport, FlowControl, CloseSide) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), Opts0 = #{ transport => OriginTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], protocols => [raw] }, Opts = do_maybe_add_flow(FlowControl, Opts0), {ok, ConnPid} = gun:open("localhost", OriginPort, Opts), {ok, raw} = gun:await_up(ConnPid), handshake_completed = receive_from(OriginPid), %% When we take over the entire connection there is no stream reference. gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), do_flow_control(FlowControl, ConnPid), #{ transport := OriginTransport, protocol := raw, origin_scheme := undefined, origin_host := "localhost", origin_port := OriginPort, intermediaries := [] } = gun:info(ConnPid), do_close(CloseSide, ConnPid). do_maybe_add_flow(flow_control_enabled, Opts) -> Opts#{raw_opts => #{flow => 1}}; do_maybe_add_flow(flow_control_disabled, Opts) -> Opts. do_flow_control(flow_control_enabled, ConnPid) -> gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), {error, timeout} = gun:await(ConnPid, undefined, 1000), ok = gun:update_flow(ConnPid, undefined, 1), {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined); do_flow_control(flow_control_disabled, _ConnPid) -> ok. do_close(client_side_close, ConnPid) -> gun:close(ConnPid); do_close(server_side_close, ConnPid) -> gun:data(ConnPid, undefined, nofin, <<"close">>), receive {gun_down, ConnPid, raw, closed, []} -> ok after 1000 -> error(timeout) end. socks5_tcp_raw_tcp(_) -> doc("Use Socks5 over TCP to connect to a remote endpoint using the raw protocol over TCP."), do_socks5_raw(tcp, tcp). socks5_tcp_raw_tls(_) -> doc("Use Socks5 over TCP to connect to a remote endpoint using the raw protocol over TLS."), do_socks5_raw(tcp, tls). socks5_tls_raw_tcp(_) -> doc("Use Socks5 over TLS to connect to a remote endpoint using the raw protocol over TCP."), do_socks5_raw(tls, tcp). socks5_tls_raw_tls(_) -> doc("Use Socks5 over TLS to connect to a remote endpoint using the raw protocol over TLS."), do_socks5_raw(tls, tls). do_socks5_raw(OriginTransport, ProxyTransport) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), {ok, ProxyPid, ProxyPort} = socks_SUITE:do_proxy_start(ProxyTransport, none), {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ transport => ProxyTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], protocols => [{socks, #{ host => "localhost", port => OriginPort, transport => OriginTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], protocols => [raw] }}] }), %% We receive a gun_up and a gun_tunnel_up. {ok, socks} = gun:await_up(ConnPid), {up, raw} = gun:await(ConnPid, undefined), %% The proxy received two packets. {auth_methods, 1, [none]} = receive_from(ProxyPid), {connect, <<"localhost">>, OriginPort} = receive_from(ProxyPid), handshake_completed = receive_from(OriginPid), %% When we take over the entire connection there is no stream reference. gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), #{ transport := OriginTransport, protocol := raw, origin_scheme := undefined, origin_host := "localhost", origin_port := OriginPort, intermediaries := [#{ type := socks5, host := "localhost", port := ProxyPort, transport := ProxyTransport, protocol := socks }]} = gun:info(ConnPid), gun:close(ConnPid). connect_tcp_raw_tcp(_) -> doc("Use CONNECT over TCP to connect to a remote endpoint using the raw protocol over TCP."), do_connect_raw(tcp, tcp). connect_tcp_raw_tls(_) -> doc("Use CONNECT over TCP to connect to a remote endpoint using the raw protocol over TLS."), do_connect_raw(tcp, tls). connect_tls_raw_tcp(_) -> doc("Use CONNECT over TLS to connect to a remote endpoint using the raw protocol over TCP."), do_connect_raw(tls, tcp). connect_tls_raw_tls(_) -> doc("Use CONNECT over TLS to connect to a remote endpoint using the raw protocol over TLS."), do_connect_raw(tls, tls). do_connect_raw(OriginTransport, ProxyTransport) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), {ok, ProxyPid, ProxyPort} = rfc7231_SUITE:do_proxy_start(ProxyTransport), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ transport => ProxyTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] }), {ok, http} = gun:await_up(ConnPid), StreamRef = gun:connect(ConnPid, #{ host => "localhost", port => OriginPort, transport => OriginTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], protocols => [raw] }), {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), {response, fin, 200, _} = gun:await(ConnPid, StreamRef), handshake_completed = receive_from(OriginPid), {up, raw} = gun:await(ConnPid, StreamRef), gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>), {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef), #{ transport := OriginTransport, protocol := raw, origin_scheme := undefined, origin_host := "localhost", origin_port := OriginPort, intermediaries := [#{ type := connect, host := "localhost", port := ProxyPort, transport := ProxyTransport, protocol := http }]} = gun:info(ConnPid), gun:close(ConnPid). connect_raw_reply_to(_) -> doc("When using CONNECT to establish a connection with the reply_to option set, " "Gun must honor this option in the raw protocol."), Self = self(), ReplyTo = spawn(fun() -> {ConnPid, StreamRef} = receive Msg -> Msg after 1000 -> error(timeout) end, {response, fin, 200, _} = gun:await(ConnPid, StreamRef), {up, raw} = gun:await(ConnPid, StreamRef), Self ! {self(), ready}, {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef), Self ! {self(), ok} end), {ok, OriginPid, OriginPort} = init_origin(tcp, raw, fun do_echo/4), {ok, ProxyPid, ProxyPort} = rfc7231_SUITE:do_proxy_start(tcp), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), StreamRef = gun:connect(ConnPid, #{ host => "localhost", port => OriginPort, protocols => [raw] }, [], #{reply_to => ReplyTo}), ReplyTo ! {ConnPid, StreamRef}, {request, <<"CONNECT">>, _, 'HTTP/1.1', _} = receive_from(ProxyPid), handshake_completed = receive_from(OriginPid), receive {ReplyTo, ready} -> ok after 1000 -> error(timeout) end, gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>), receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end. http11_upgrade_raw_tcp(_) -> doc("Use the HTTP Upgrade mechanism to switch to the raw protocol over TCP."), do_http11_upgrade_raw(tcp). http11_upgrade_raw_tls(_) -> doc("Use the HTTP Upgrade mechanism to switch to the raw protocol over TLS."), do_http11_upgrade_raw(tls). do_http11_upgrade_raw(OriginTransport) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun (Parent, ListenSocket, ClientSocket, ClientTransport) -> %% We skip the request and send a 101 response unconditionally. {ok, _} = ClientTransport:recv(ClientSocket, 0, 5000), ClientTransport:send(ClientSocket, "HTTP/1.1 101 Switching Protocols\r\n" "Connection: upgrade\r\n" "Upgrade: custom/1.0\r\n" "\r\n"), do_echo(Parent, ListenSocket, ClientSocket, ClientTransport) end), {ok, ConnPid} = gun:open("localhost", OriginPort, #{ transport => OriginTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] }), {ok, http} = gun:await_up(ConnPid), handshake_completed = receive_from(OriginPid), StreamRef = gun:get(ConnPid, "/", #{ <<"connection">> => <<"upgrade">>, <<"upgrade">> => <<"custom/1.0">> }), {upgrade, [<<"custom/1.0">>], _} = gun:await(ConnPid, StreamRef), %% When we take over the entire connection there is no stream reference. gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), #{ transport := OriginTransport, protocol := raw, origin_scheme := undefined, origin_host := "localhost", origin_port := OriginPort, intermediaries := [] } = gun:info(ConnPid), gun:close(ConnPid). http11_upgrade_raw_reply_to(_) -> doc("When upgrading an HTTP/1.1 connection with the reply_to option set, " "Gun must honor this option in the raw protocol."), Self = self(), ReplyTo = spawn(fun() -> {ConnPid, StreamRef} = receive Msg -> Msg after 1000 -> error(timeout) end, {upgrade, [<<"custom/1.0">>], _} = gun:await(ConnPid, StreamRef), Self ! {self(), ready}, {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), Self ! {self(), ok} end), {ok, OriginPid, OriginPort} = init_origin(tcp, raw, fun (Parent, ListenSocket, ClientSocket, ClientTransport) -> %% We skip the request and send a 101 response unconditionally. {ok, _} = ClientTransport:recv(ClientSocket, 0, 5000), ClientTransport:send(ClientSocket, "HTTP/1.1 101 Switching Protocols\r\n" "Connection: upgrade\r\n" "Upgrade: custom/1.0\r\n" "\r\n"), do_echo(Parent, ListenSocket, ClientSocket, ClientTransport) end), {ok, ConnPid} = gun:open("localhost", OriginPort), {ok, http} = gun:await_up(ConnPid), handshake_completed = receive_from(OriginPid), StreamRef = gun:get(ConnPid, "/", #{ <<"connection">> => <<"upgrade">>, <<"upgrade">> => <<"custom/1.0">> }, #{reply_to => ReplyTo}), ReplyTo ! {ConnPid, StreamRef}, receive {ReplyTo, ready} -> ok after 1000 -> error(timeout) end, gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end. http2_connect_tcp_raw_tcp(_) -> doc("Use CONNECT over clear HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."), do_http2_connect_raw(tcp, <<"http">>, tcp). http2_connect_tls_raw_tcp(_) -> doc("Use CONNECT over secure HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."), do_http2_connect_raw(tcp, <<"https">>, tls). do_http2_connect_raw(OriginTransport, ProxyScheme, ProxyTransport) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), {ok, ProxyPid, ProxyPort} = rfc7540_SUITE:do_proxy_start(ProxyTransport, [ {proxy_stream, 1, 200, [], 0, undefined} ]), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ transport => ProxyTransport, tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], protocols => [http2] }), {ok, http2} = gun:await_up(ConnPid), handshake_completed = receive_from(ProxyPid), StreamRef = gun:connect(ConnPid, #{ host => "localhost", port => OriginPort, transport => OriginTransport, protocols => [raw] }), {request, #{ <<":method">> := <<"CONNECT">>, <<":authority">> := Authority }} = receive_from(ProxyPid), {response, fin, 200, _} = gun:await(ConnPid, StreamRef), handshake_completed = receive_from(OriginPid), {up, raw} = gun:await(ConnPid, StreamRef), gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>), {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef), #{ transport := ProxyTransport, protocol := http2, origin_scheme := ProxyScheme, origin_host := "localhost", origin_port := ProxyPort, intermediaries := [] } = gun:info(ConnPid), Self = self(), {ok, #{ ref := StreamRef, reply_to := Self, state := running, tunnel := #{ transport := OriginTransport, protocol := raw, origin_scheme := undefined, origin_host := "localhost", origin_port := OriginPort } }} = gun:stream_info(ConnPid, StreamRef), gun:close(ConnPid). %% The origin server will echo everything back. do_echo(Parent, ListenSocket, ClientSocket, ClientTransport) -> case ClientTransport:recv(ClientSocket, 0, 5000) of {ok, <<"close">>} -> ok = ClientTransport:close(ClientSocket); {ok, Data} -> ClientTransport:send(ClientSocket, Data), do_echo(Parent, ListenSocket, ClientSocket, ClientTransport); {error, closed} -> ok end.