Skip to content

Commit c8d82d1

Browse files
nickvawillholley
andauthored
feat: add connect_to option (#183)
* feat: add connect_to option Add a `connect_to` request option that lets ibrowse connect to a different network target while preserving the original URL host for Host handling, cookies, and TLS SNI. The use case for this is when connecting through a transparent SNI proxy for egress control. * Add connect_to tests These are for Will's "feat: add connect_to option" feature --------- Co-authored-by: Will Holley <will.holley@uk.ibm.com>
1 parent 2772261 commit c8d82d1

3 files changed

Lines changed: 98 additions & 7 deletions

File tree

src/ibrowse_http_client.erl

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -580,13 +580,18 @@ do_connect(Host, Port, Options, #state{is_ssl = true,
580580
use_proxy = false,
581581
ssl_options = SSLOptions},
582582
Timeout) ->
583+
%% Check for connect_to override and remove from options
584+
Host1 = get_value(connect_to, Options, Host),
585+
Options1 = proplists:delete(connect_to, Options),
586+
583587
%% if a socks5 proxy is configured, open the socket separately
584588
%% before upgrading the socket to a TLS connection.
585-
case get_value(socks5_host, Options, undefined) of
589+
case get_value(socks5_host, Options1, undefined) of
586590
%% no socks5 proxy is configured, connect directly with TLS:
587591
undefined ->
588-
Sock_options = get_sock_options(Host, Options, SSLOptions),
589-
ssl:connect(Host, Port, Sock_options, Timeout);
592+
Sock_options = get_sock_options(Host, Options1, SSLOptions),
593+
Sock_options1 = ensure_sni(Sock_options, Host),
594+
ssl:connect(Host1, Port, Sock_options1, Timeout);
590595

591596
%% proxy configuration is present: first establish a socket
592597
%% and then upgrade:
@@ -603,13 +608,17 @@ do_connect(Host, Port, Options, #state{is_ssl = true,
603608
end;
604609

605610
do_connect(Host, Port, Options, _State, Timeout) ->
606-
Socks5Host = get_value(socks5_host, Options, undefined),
607-
Sock_options = get_sock_options(Host, Options, []),
611+
%% Check for connect_to override and remove from options
612+
Host1 = get_value(connect_to, Options, Host),
613+
Options1 = proplists:delete(connect_to, Options),
614+
615+
Socks5Host = get_value(socks5_host, Options1, undefined),
616+
Sock_options = get_sock_options(Host, Options1, []),
608617
case Socks5Host of
609618
undefined ->
610-
gen_tcp:connect(Host, Port, Sock_options, Timeout);
619+
gen_tcp:connect(Host1, Port, Sock_options, Timeout);
611620
_ ->
612-
catch ibrowse_socks5:connect(Host, Port, Options, Sock_options, Timeout)
621+
catch ibrowse_socks5:connect(Host1, Port, Options1, Sock_options, Timeout)
613622
end.
614623

615624
get_sock_options(Host, Options, SSLOptions) ->
@@ -657,10 +666,20 @@ filter_sock_options(Opts) ->
657666
false;
658667
(list) ->
659668
false;
669+
660670
(_) ->
661671
true
662672
end, Opts).
663673

674+
%% Ensure SNI is set for SSL connections when using connect_to override
675+
ensure_sni(Opts, Host) ->
676+
case lists:keyfind(server_name_indication, 1, Opts) of
677+
false ->
678+
[{server_name_indication, Host} | Opts];
679+
_ ->
680+
Opts
681+
end.
682+
664683
do_send(Req, #state{socket = Sock,
665684
is_ssl = true,
666685
use_proxy = true,

test/ibrowse_test.erl

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
test_preserve_status_line/0,
3535
test_binary_headers/0,
3636
test_binary_headers/1,
37+
test_connect_to_overrides_target/0,
38+
test_connect_to_unreachable_fails/0,
39+
test_connect_to_preserves_host_header/0,
3740
test_dead_lb_pid/0,
3841
test_generate_body_0/0,
3942
test_retry_of_requests/0,
@@ -62,6 +65,9 @@
6265
{local_test_fun, test_303_response_with_a_body, []},
6366
{local_test_fun, test_303_response_with_no_body, []},
6467
{local_test_fun, test_binary_headers, []},
68+
{local_test_fun, test_connect_to_overrides_target, []},
69+
{local_test_fun, test_connect_to_unreachable_fails, []},
70+
{local_test_fun, test_connect_to_preserves_host_header, []},
6571
{local_test_fun, test_dead_lb_pid, []},
6672
{local_test_fun, test_retry_of_requests, []},
6773
{local_test_fun, verify_chunked_streaming, []},
@@ -546,6 +552,55 @@ test_binary_headers(Url) ->
546552
{test_failed, Res}
547553
end.
548554

555+
%%------------------------------------------------------------------------------
556+
%% url host is bogus, request only succeeds if connect_to actually
557+
%% redirects
558+
%%------------------------------------------------------------------------------
559+
test_connect_to_overrides_target() ->
560+
clear_msg_q(),
561+
Url = "http://no.such.host.invalid:8181/",
562+
case ibrowse:send_req(Url, [], get, [], [{connect_to, "localhost"}], 5000) of
563+
{ok, "200", _, _} ->
564+
success;
565+
Res ->
566+
{test_failed, Res}
567+
end.
568+
569+
%%------------------------------------------------------------------------------
570+
%% url host reachable, but connect_to points at an unroutable
571+
%% address
572+
%%------------------------------------------------------------------------------
573+
test_connect_to_unreachable_fails() ->
574+
clear_msg_q(),
575+
case ibrowse:send_req("http://localhost:8181/", [], get, [],
576+
[{connect_to, "192.0.2.1"},
577+
{connect_timeout, 500}],
578+
5000) of
579+
{error, _} ->
580+
success;
581+
Res ->
582+
{test_failed, Res}
583+
end.
584+
585+
%%------------------------------------------------------------------------------
586+
%% connect_to shouldn't bleed into the Host header, server should see the
587+
%% original url host
588+
%%------------------------------------------------------------------------------
589+
test_connect_to_preserves_host_header() ->
590+
clear_msg_q(),
591+
Url = "http://example.invalid:8181/ibrowse_echo_host",
592+
case ibrowse:send_req(Url, [], get, [], [{connect_to, "localhost"}], 5000) of
593+
{ok, "200", Headers, _} ->
594+
case proplists:get_value("x-host", Headers) of
595+
"example.invalid:8181" ->
596+
success;
597+
V ->
598+
{fail, V}
599+
end;
600+
Res ->
601+
{test_failed, Res}
602+
end.
603+
549604
%%------------------------------------------------------------------------------
550605
%% Test what happens when the response to a HEAD request is a
551606
%% Chunked-Encoding response with a non-empty body. Issue #67 on

test/ibrowse_test_server.erl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ process_request(Sock, Sock_type,
229229
false ->
230230
collect_body
231231
end;
232+
process_request(Sock, Sock_type,
233+
#request{method='GET',
234+
headers = Headers,
235+
uri = {abs_path, "/ibrowse_echo_host"}}) ->
236+
Host = get_host_header(Headers),
237+
Resp = [<<"HTTP/1.1 200 OK\r\n">>,
238+
<<"Server: ibrowse_test\r\n">>,
239+
"x-host: ", Host, "\r\n",
240+
<<"Content-Length: 0\r\n\r\n">>],
241+
do_send(Sock, Sock_type, Resp);
232242
process_request(Sock, Sock_type,
233243
#request{method='GET',
234244
headers = Headers,
@@ -366,3 +376,10 @@ get_content_length([{http_header, _, _X, _, _Y} | T]) ->
366376
get_content_length(T);
367377
get_content_length([]) ->
368378
undefined.
379+
380+
get_host_header([{http_header, _, 'Host', _, V} | _]) ->
381+
V;
382+
get_host_header([_ | T]) ->
383+
get_host_header(T);
384+
get_host_header([]) ->
385+
"not_found".

0 commit comments

Comments
 (0)