diff --git a/src/lhttpc.app.src b/src/lhttpc.app.src index e853322a..32c8d0b0 100644 --- a/src/lhttpc.app.src +++ b/src/lhttpc.app.src @@ -24,7 +24,7 @@ %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %%% ---------------------------------------------------------------------------- -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @doc This is the specification for the lhttpc application. %%% @end {application, lhttpc, diff --git a/src/lhttpc.erl b/src/lhttpc.erl index d269c579..86b5f071 100644 --- a/src/lhttpc.erl +++ b/src/lhttpc.erl @@ -24,7 +24,7 @@ %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %%% ---------------------------------------------------------------------------- -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @doc Main interface to the lightweight http client. %%% See {@link request/4}, {@link request/5} and {@link request/6} functions. -module(lhttpc). diff --git a/src/lhttpc_client.erl b/src/lhttpc_client.erl index f33f34d3..9dc92083 100644 --- a/src/lhttpc_client.erl +++ b/src/lhttpc_client.erl @@ -25,7 +25,7 @@ %%% ---------------------------------------------------------------------------- %%% @private -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @doc %%% This module implements the HTTP request handling. This should normally %%% not be called directly since it should be spawned by the lhttpc module. @@ -105,30 +105,26 @@ 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), + + %% this can be target host's or proxy's host/port/ssl !?!? + %% How do we handle situation like + %% > request(host1, proxy1) + %% > request(host1, proxy2) + %% ??? SocketRequest = {socket, self(), Host, Port, Ssl}, Pool = proplists:get_value(pool, Options, whereis(lhttpc_manager)), Socket = case gen_server:call(Pool, SocketRequest, infinity) of {ok, S} -> S; % Re-using HTTP/1.1 connections no_socket -> undefined % Opening a new HTTP/1.1 connection end, + + State = #client_state{ host = Host, port = Port, ssl = Ssl, method = NormalizedMethod, - request = Request, + %% request = Request, requester = From, request_headers = Hdrs, socket = Socket, @@ -138,17 +134,21 @@ 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, + %% 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 = 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} -> @@ -171,6 +171,65 @@ 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}. + + send_request(#client_state{attempts = 0}) -> % Don't try again if the number of allowed attempts is 0. throw(connection_closed); diff --git a/src/lhttpc_lib.erl b/src/lhttpc_lib.erl index f02d2136..edd471fd 100644 --- a/src/lhttpc_lib.erl +++ b/src/lhttpc_lib.erl @@ -25,13 +25,14 @@ %%% ---------------------------------------------------------------------------- %%% @private -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @doc %%% This module implements various library functions used in lhttpc. -module(lhttpc_lib). -export([ parse_url/1, + format_url/1, format_request/7, header_value/2, header_value/3, @@ -96,6 +97,49 @@ maybe_atom_to_list(Atom) when is_atom(Atom) -> maybe_atom_to_list(List) -> List. +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/src/lhttpc_manager.erl b/src/lhttpc_manager.erl index 65a76976..9ce27805 100644 --- a/src/lhttpc_manager.erl +++ b/src/lhttpc_manager.erl @@ -24,7 +24,7 @@ %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %%% ---------------------------------------------------------------------------- -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @author Filipe David Manana %%% @doc Connection manager for the HTTP client. %%% This gen_server is responsible for keeping track of persistent diff --git a/src/lhttpc_sock.erl b/src/lhttpc_sock.erl index 507f74a2..abf5d524 100644 --- a/src/lhttpc_sock.erl +++ b/src/lhttpc_sock.erl @@ -25,7 +25,7 @@ %%% ---------------------------------------------------------------------------- %%% @private -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @doc %%% This module implements wrappers for socket operations. %%% Makes it possible to have the same interface to ssl and tcp sockets. diff --git a/src/lhttpc_sup.erl b/src/lhttpc_sup.erl index 3dca9582..5663a3e2 100644 --- a/src/lhttpc_sup.erl +++ b/src/lhttpc_sup.erl @@ -24,7 +24,7 @@ %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %%% ---------------------------------------------------------------------------- -%%% @author Oscar Hellström +%%% @author Oscar Hellström %%% @doc Top supervisor for the lhttpc application. %%% This is normally started by the application behaviour implemented in %%% {@link lhttpc}. diff --git a/test/lhttpc_lib_tests.erl b/test/lhttpc_lib_tests.erl index cb688127..acaa0243 100644 --- a/test/lhttpc_lib_tests.erl +++ b/test/lhttpc_lib_tests.erl @@ -201,5 +201,95 @@ parse_url_test_() -> user = "joe", password = "erlang" }, - lhttpc_lib:parse_url("http://joe:erlang@[1080:0:0:0:8:800:200C:417A]:180/foo/bar")) + lhttpc_lib:parse_url("http://joe:erlang@[1080:0:0:0:8:800:200C:417A]:180/foo/bar")), + + %% 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" + })) ].