diff --git a/src/lhttpc_client.erl b/src/lhttpc_client.erl index 3a159120..501f50ff 100644 --- a/src/lhttpc_client.erl +++ b/src/lhttpc_client.erl @@ -124,19 +124,7 @@ execute(From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) -> PartialDownload = proplists:is_defined(partial_download, Options), PartialDownloadOptions = proplists:get_value(partial_download, Options, []), NormalizedMethod = lhttpc_lib:normalize_method(Method), - Proxy = case proplists:get_value(proxy, Options) of - undefined -> - undefined; - ProxyUrl when is_list(ProxyUrl), not Ssl -> - % The point of HTTP CONNECT proxying is to use TLS tunneled in - % a plain HTTP/1.1 connection to the proxy (RFC2817). - throw(origin_server_not_https); - ProxyUrl when is_list(ProxyUrl) -> - lhttpc_lib:parse_url(ProxyUrl) - end, - {ChunkedUpload, Request} = lhttpc_lib:format_request(Path, NormalizedMethod, - Hdrs, Host, Port, Body, PartialUpload), - %SocketRequest = {socket, self(), Host, Port, Ssl}, + Pool = proplists:get_value(pool, Options, whereis(lhttpc_manager)), %% Get a socket for the pool or exit %Socket = lhttpc_manager:ensure_call(Pool, SocketRequest, Options), @@ -146,7 +134,6 @@ execute(From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) -> port = Port, ssl = Ssl, method = NormalizedMethod, - request = Request, requester = From, request_headers = Hdrs, socket = Socket, @@ -156,17 +143,18 @@ execute(From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) -> attempts = 1 + proplists:get_value(send_retry, Options, 1), partial_upload = PartialUpload, upload_window = UploadWindowSize, - chunked_upload = ChunkedUpload, partial_download = PartialDownload, download_window = proplists:get_value(window_size, PartialDownloadOptions, infinity), part_size = proplists:get_value(part_size, PartialDownloadOptions, infinity), - proxy = Proxy, - proxy_setup = (Socket =/= undefined), proxy_ssl_options = proplists:get_value(proxy_ssl_options, Options, []) }, - Response = case send_request(State) of + + State2 = configure_proxy(State, Body, Path, Ssl, + proplists:get_value(proxy, Options)), + + Response = case send_request(State2) of {R, undefined} -> {ok, R}; {R, NewSocket} -> @@ -183,6 +171,67 @@ execute(From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) -> end, {response, self(), Response}. + +configure_proxy(State, Body, Path, _Ssl, undefined) -> + %% no proxy at all + set_request(State#client_state{proxy=undefined}, Body, Path); +configure_proxy(State, Body, Path, true, ProxyUrl) when is_list(ProxyUrl) -> + %% connect through ssl/not ssl proxy to ssl host using CONNECT + Proxy = lhttpc_lib:parse_url(ProxyUrl), + set_request(State#client_state{ + proxy = Proxy, + proxy_setup = (State#client_state.socket =/= undefined)}, + Body, Path); +configure_proxy(State, Body, Path, false, ProxyUrl) -> + %% connect through ssl/not ssl proxy to not ssl host + %% just replace Path with full host URL + #client_state{request_headers=Hdrs, + host=DestHost, + port=DestPort} = State, + Proxy = lhttpc_lib:parse_url(ProxyUrl), + #lhttpc_url{ + %% host = Host, + %% port = Port, + user = User, + password = Passwd + %% is_ssl = SslProxy + } = Proxy, + NewPath = lhttpc_lib:format_url( + #lhttpc_url{host=DestHost, + port=DestPort, + path=Path, + is_ssl=false}), + Hdrs2 = case User of + "" -> + Hdrs; + User -> + AuthHdr = {"Proxy-Authorization", + "Basic " ++ base64:encode_to_string(User ++ ":" ++ Passwd)}, + [AuthHdr | Hdrs] + end, + set_request(State#client_state{ + %% ssl = SslProxy, % see request_first_destination + %% host = Host, + %% port = Port, + request_headers = Hdrs2, + proxy = Proxy, + proxy_setup = true}, + Body, NewPath). + + +set_request(State, Body, Path) -> + #client_state{partial_upload=PartialUpload, + method=NormalizedMethod, + request_headers=Hdrs, + host=Host, + port=Port} = State, + {ChunkedUpload, Request} = lhttpc_lib:format_request( + Path, NormalizedMethod, Hdrs, Host, Port, Body, PartialUpload), + State#client_state{request=Request, + chunked_upload = ChunkedUpload}. + + + %%------------------------------------------------------------------------------ %% @private %% @doc This function creates a new socket connection if needed, and it also diff --git a/src/lhttpc_lib.erl b/src/lhttpc_lib.erl index a83123b1..50880186 100644 --- a/src/lhttpc_lib.erl +++ b/src/lhttpc_lib.erl @@ -33,6 +33,7 @@ -module(lhttpc_lib). -export([parse_url/1, + format_url/1, format_request/7, header_value/2, header_value/3, normalize_method/1, @@ -113,6 +114,54 @@ maybe_atom_to_list(List) -> List. %%------------------------------------------------------------------------------ +%% @spec (#lhttpc_url{}) -> string() +%% @doc +%% Format parsed URL to string representation. +%% @end +%%------------------------------------------------------------------------------ +format_url(URL) -> + #lhttpc_url{ + host = Host, + port = Port, + path = Path, + user = User, + password = Passwd, + is_ssl = IsSSL + } = URL, + U1 = add_scheme("", IsSSL), + U2 = add_credentials(U1, User, Passwd), + U3 = add_host(U2, Host), + U4 = add_port(U3, Port, IsSSL), + lists:flatten(add_path(U4, Path)). + +add_scheme(_, true) -> + "https://"; +add_scheme(_, false) -> + "http://". + +add_credentials(Scheme, "", "") -> + Scheme; +add_credentials(Scheme, User, "") -> + [Scheme, User, "@"]; +add_credentials(Scheme, User, Passwd) -> + [Scheme, User, ":", Passwd, "@"]. + +add_host(SUP, Host) -> + Host2 = maybe_ipv6_enclose(Host), + [SUP, Host2]. + +add_port(SUPH, 80, false) -> + SUPH; +add_port(SUPH, 443, true) -> + SUPH; +add_port(SUPH, Port, _IsSSL) -> + [SUPH, ":", integer_to_list(Port)]. + +add_path(SUPHP, Path) -> + [SUPHP, Path]. + + + %% @spec (URL) -> #lhttpc_url{} %% URL = string() %% @doc diff --git a/test/lhttpc_lib_tests.erl b/test/lhttpc_lib_tests.erl index c8e135a3..249376cc 100644 --- a/test/lhttpc_lib_tests.erl +++ b/test/lhttpc_lib_tests.erl @@ -222,5 +222,95 @@ parse_url_test_() -> user = "", password = "" }, - lhttpc_lib:parse_url("http://www.example.com?a=b")) + lhttpc_lib:parse_url("http://www.example.com?a=b")), + + %% Serialisation + ?_assertEqual("http://host/", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 80, + path = "/", + is_ssl = false, + user = "", + password = "" + })), + + ?_assertEqual("https://host/", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 443, + path = "/", + is_ssl = true, + user = "", + password = "" + })), + + ?_assertEqual("http://host:180/", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 180, + path = "/", + is_ssl = false, + user = "", + password = "" + })), + + ?_assertEqual("https://host:180/", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 180, + path = "/", + is_ssl = true, + user = "", + password = "" + })), + + ?_assertEqual("http://host:180/path", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 180, + path = "/path", + is_ssl = false, + user = "", + password = "" + })), + + ?_assertEqual("http://user@host:180/path", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 180, + path = "/path", + is_ssl = false, + user = "user", + password = "" + })), + + ?_assertEqual("http://user:pass@host:180/path", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 180, + path = "/path", + is_ssl = false, + user = "user", + password = "pass" + })), + ?_assertEqual("http://:pass@host:180/path", + lhttpc_lib:format_url(#lhttpc_url{ + host = "host", + port = 180, + path = "/path", + is_ssl = false, + user = "", + password = "pass" + })), + + ?_assertEqual("http://user:pass@[::1]:180/path", + lhttpc_lib:format_url(#lhttpc_url{ + host = "::1", + port = 180, + path = "/path", + is_ssl = false, + user = "user", + password = "pass" + })) ].