From 6153ba7599f2ce1ab22959a40b6ca33b4238f0d0 Mon Sep 17 00:00:00 2001 From: Micael Karlberg Date: Wed, 13 Jan 2010 16:18:24 +0000 Subject: OTP-8016, OTP-8056, OTP-8103, OTP-8106, OTP-8312, OTP-8315, OTP-8327, OTP-8349, OTP-8351, OTP-8359 & OTP-8371. --- lib/inets/src/ftp/Makefile | 18 +- lib/inets/src/http_client/http_cookie.erl | 391 --------- lib/inets/src/http_client/httpc.erl | 1030 +++++++++++++++++++++++ lib/inets/src/http_client/httpc_cookie.erl | 495 +++++++++++ lib/inets/src/http_client/httpc_handler.erl | 767 +++++++++-------- lib/inets/src/http_client/httpc_handler_sup.erl | 31 +- 6 files changed, 1992 insertions(+), 740 deletions(-) delete mode 100644 lib/inets/src/http_client/http_cookie.erl create mode 100644 lib/inets/src/http_client/httpc.erl create mode 100644 lib/inets/src/http_client/httpc_cookie.erl (limited to 'lib/inets/src') diff --git a/lib/inets/src/ftp/Makefile b/lib/inets/src/ftp/Makefile index 70d51115e6..0c15277a18 100644 --- a/lib/inets/src/ftp/Makefile +++ b/lib/inets/src/ftp/Makefile @@ -1,19 +1,19 @@ # # %CopyrightBegin% -# -# Copyright Ericsson AB 2005-2009. All Rights Reserved. -# +# +# Copyright Ericsson AB 2005-2010. All Rights Reserved. +# # The contents of this file are subject to the Erlang Public License, # Version 1.1, (the "License"); you may not use this file except in # compliance with the License. You should have received a copy of the # Erlang Public License along with this software. If not, it can be # retrieved online at http://www.erlang.org/. -# +# # Software distributed under the License is distributed on an "AS IS" # basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See # the License for the specific language governing rights and limitations # under the License. -# +# # %CopyrightEnd% # # @@ -32,7 +32,8 @@ VSN = $(INETS_VSN) # ---------------------------------------------------- # Release directory specification # ---------------------------------------------------- -RELSYSDIR = $(RELEASE_PATH)/lib/inets-$(VSN) +RELSYSDIR = $(RELEASE_PATH)/lib/$(APPLICATION)-$(VSN) + # ---------------------------------------------------- # Target Specs @@ -49,15 +50,17 @@ ERL_FILES = $(MODULES:%=%.erl) TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) + # ---------------------------------------------------- # INETS FLAGS # ---------------------------------------------------- -INETS_FLAGS = -D'SERVER_SOFTWARE="inets/$(VSN)"' +INETS_FLAGS = -D'SERVER_SOFTWARE="$(APPLICATION)/$(VSN)"' ifeq ($(FTP_DEBUG),true) INETS_FLAGS += -Dftp_debug endif + # ---------------------------------------------------- # FLAGS # ---------------------------------------------------- @@ -94,6 +97,7 @@ release_spec: opt release_docs_spec: info: + @echo "APPLICATION = $(APPLICATION)" @echo "INETS_DEBUG = $(INETS_DEBUG)" @echo "INETS_FLAGS = $(INETS_FLAGS)" @echo "ERL_COMPILE_FLAGS = $(ERL_COMPILE_FLAGS)" diff --git a/lib/inets/src/http_client/http_cookie.erl b/lib/inets/src/http_client/http_cookie.erl deleted file mode 100644 index e091070f72..0000000000 --- a/lib/inets/src/http_client/http_cookie.erl +++ /dev/null @@ -1,391 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2004-2009. All Rights Reserved. -%% -%% The contents of this file are subject to the Erlang Public License, -%% Version 1.1, (the "License"); you may not use this file except in -%% compliance with the License. You should have received a copy of the -%% Erlang Public License along with this software. If not, it can be -%% retrieved online at http://www.erlang.org/. -%% -%% Software distributed under the License is distributed on an "AS IS" -%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%% the License for the specific language governing rights and limitations -%% under the License. -%% -%% %CopyrightEnd% -%% -%% Description: Cookie handling according to RFC 2109 - --module(http_cookie). - --include("httpc_internal.hrl"). - --export([header/4, cookies/3, open_cookie_db/1, close_cookie_db/1, insert/2]). - -%%%========================================================================= -%%% API -%%%========================================================================= -header(Scheme, {Host, _}, Path, CookieDb) -> - case lookup_cookies(Host, Path, CookieDb) of - [] -> - {"cookie", ""}; - Cookies -> - {"cookie", cookies_to_string(Scheme, Cookies)} - end. - -cookies(Headers, RequestPath, RequestHost) -> - Cookies = parse_set_cookies(Headers, {RequestPath, RequestHost}), - accept_cookies(Cookies, RequestPath, RequestHost). - -open_cookie_db({{_, only_session_cookies}, SessionDbName}) -> - EtsDb = ets:new(SessionDbName, [protected, bag, - {keypos, #http_cookie.domain}]), - {undefined, EtsDb}; - -open_cookie_db({{DbName, Dbdir}, SessionDbName}) -> - File = filename:join(Dbdir, atom_to_list(DbName)), - {ok, DetsDb} = dets:open_file(DbName, [{keypos, #http_cookie.domain}, - {type, bag}, - {file, File}, - {ram_file, true}]), - EtsDb = ets:new(SessionDbName, [protected, bag, - {keypos, #http_cookie.domain}]), - {DetsDb, EtsDb}. - -close_cookie_db({undefined, EtsDb}) -> - ets:delete(EtsDb); - -close_cookie_db({DetsDb, EtsDb}) -> - dets:close(DetsDb), - ets:delete(EtsDb). - -%% If no persistent cookie database is defined we -%% treat all cookies as if they where session cookies. -insert(Cookie = #http_cookie{max_age = Int}, - Dbs = {undefined, _}) when is_integer(Int) -> - insert(Cookie#http_cookie{max_age = session}, Dbs); - -insert(Cookie = #http_cookie{domain = Key, name = Name, - path = Path, max_age = session}, - Db = {_, CookieDb}) -> - case ets:match_object(CookieDb, #http_cookie{domain = Key, - name = Name, - path = Path, - _ = '_'}) of - [] -> - ets:insert(CookieDb, Cookie); - [NewCookie] -> - delete(NewCookie, Db), - ets:insert(CookieDb, Cookie) - end, - ok; -insert(#http_cookie{domain = Key, name = Name, - path = Path, max_age = 0}, - Db = {CookieDb, _}) -> - case dets:match_object(CookieDb, #http_cookie{domain = Key, - name = Name, - path = Path, - _ = '_'}) of - [] -> - ok; - [NewCookie] -> - delete(NewCookie, Db) - end, - ok; -insert(Cookie = #http_cookie{domain = Key, name = Name, path = Path}, - Db = {CookieDb, _}) -> - case dets:match_object(CookieDb, #http_cookie{domain = Key, - name = Name, - path = Path, - _ = '_'}) of - [] -> - dets:insert(CookieDb, Cookie); - [NewCookie] -> - delete(NewCookie, Db), - dets:insert(CookieDb, Cookie) - end, - ok. - -%%%======================================================================== -%%% Internal functions -%%%======================================================================== -lookup_cookies(Key, {undefined, Ets}) -> - ets:match_object(Ets, #http_cookie{domain = Key, - _ = '_'}); -lookup_cookies(Key, {Dets,Ets}) -> - SessionCookies = ets:match_object(Ets, #http_cookie{domain = Key, - _ = '_'}), - Cookies = dets:match_object(Dets, #http_cookie{domain = Key, - _ = '_'}), - Cookies ++ SessionCookies. - -delete(Cookie = #http_cookie{max_age = session}, {_, CookieDb}) -> - ets:delete_object(CookieDb, Cookie); -delete(Cookie, {CookieDb, _}) -> - dets:delete_object(CookieDb, Cookie). - -lookup_cookies(Host, Path, Db) -> - Cookies = - case http_util:is_hostname(Host) of - true -> - HostCookies = lookup_cookies(Host, Db), - [_| DomainParts] = string:tokens(Host, "."), - lookup_domain_cookies(DomainParts, Db, HostCookies); - false -> % IP-adress - lookup_cookies(Host, Db) - end, - ValidCookies = valid_cookies(Cookies, [], Db), - lists:filter(fun(Cookie) -> - lists:prefix(Cookie#http_cookie.path, Path) - end, ValidCookies). - -%% For instance if Host=localhost -lookup_domain_cookies([], _, AccCookies) -> - lists:flatten(AccCookies); -%% Top domains can not have cookies -lookup_domain_cookies([_], _, AccCookies) -> - lists:flatten(AccCookies); -lookup_domain_cookies([Next | DomainParts], CookieDb, AccCookies) -> - Domain = merge_domain_parts(DomainParts, [Next ++ "."]), - lookup_domain_cookies(DomainParts, CookieDb, - [lookup_cookies(Domain, CookieDb) - | AccCookies]). - -merge_domain_parts([Part], Merged) -> - lists:flatten(["." | lists:reverse([Part | Merged])]); -merge_domain_parts([Part| Rest], Merged) -> - merge_domain_parts(Rest, [".", Part | Merged]). - -cookies_to_string(Scheme, Cookies = [Cookie | _]) -> - Version = "$Version=" ++ Cookie#http_cookie.version ++ "; ", - cookies_to_string(Scheme, path_sort(Cookies), [Version]). - -cookies_to_string(_, [], CookieStrs) -> - case length(CookieStrs) of - 1 -> - ""; - _ -> - lists:flatten(lists:reverse(CookieStrs)) - end; - -cookies_to_string(https, [Cookie = #http_cookie{secure = true}| Cookies], - CookieStrs) -> - Str = case Cookies of - [] -> - cookie_to_string(Cookie); - _ -> - cookie_to_string(Cookie) ++ "; " - end, - cookies_to_string(https, Cookies, [Str | CookieStrs]); - -cookies_to_string(Scheme, [#http_cookie{secure = true}| Cookies], - CookieStrs) -> - cookies_to_string(Scheme, Cookies, CookieStrs); - -cookies_to_string(Scheme, [Cookie | Cookies], CookieStrs) -> - Str = case Cookies of - [] -> - cookie_to_string(Cookie); - _ -> - cookie_to_string(Cookie) ++ "; " - end, - cookies_to_string(Scheme, Cookies, [Str | CookieStrs]). - -cookie_to_string(Cookie = #http_cookie{name = Name, value = Value}) -> - Str = Name ++ "=" ++ Value, - add_domain(add_path(Str, Cookie), Cookie). - -add_path(Str, #http_cookie{path_default = true}) -> - Str; -add_path(Str, #http_cookie{path = Path}) -> - Str ++ "; $Path=" ++ Path. - -add_domain(Str, #http_cookie{domain_default = true}) -> - Str; -add_domain(Str, #http_cookie{domain = Domain}) -> - Str ++ "; $Domain=" ++ Domain. - -parse_set_cookies(OtherHeaders, DefaultPathDomain) -> - SetCookieHeaders = lists:foldl(fun({"set-cookie", Value}, Acc) -> - [string:tokens(Value, ",")| Acc]; - (_, Acc) -> - Acc - end, [], OtherHeaders), - - lists:flatten(lists:map(fun(CookieHeader) -> - NewHeader = - fix_netscape_cookie(CookieHeader, - []), - parse_set_cookie(NewHeader, [], - DefaultPathDomain) end, - SetCookieHeaders)). - -parse_set_cookie([], AccCookies, _) -> - AccCookies; -parse_set_cookie([CookieHeader | CookieHeaders], AccCookies, - Defaults = {DefaultPath, DefaultDomain}) -> - [CookieStr | Attributes] = case string:tokens(CookieHeader, ";") of - [CStr] -> - [CStr, ""]; - [CStr | Attr] -> - [CStr, Attr] - end, - Pos = string:chr(CookieStr, $=), - Name = string:substr(CookieStr, 1, Pos - 1), - Value = string:substr(CookieStr, Pos + 1), - Cookie = #http_cookie{name = string:strip(Name), - value = string:strip(Value)}, - NewAttributes = parse_set_cookie_attributes(Attributes), - TmpCookie = cookie_attributes(NewAttributes, Cookie), - %% Add runtime defult values if necessary - NewCookie = domain_default(path_default(TmpCookie, DefaultPath), - DefaultDomain), - parse_set_cookie(CookieHeaders, [NewCookie | AccCookies], Defaults). - -parse_set_cookie_attributes([]) -> - []; -parse_set_cookie_attributes([Attributes]) -> - lists:map(fun(Attr) -> - [AttrName, AttrValue] = - case string:tokens(Attr, "=") of - %% All attributes have the form - %% Name=Value except "secure"! - [Name] -> - [Name, ""]; - [Name, Value] -> - [Name, Value]; - %% Anything not expected will be - %% disregarded - _ -> - ["Dummy",""] - end, - {http_util:to_lower(string:strip(AttrName)), - string:strip(AttrValue)} - end, Attributes). - -cookie_attributes([], Cookie) -> - Cookie; -cookie_attributes([{"comment", Value}| Attributes], Cookie) -> - cookie_attributes(Attributes, - Cookie#http_cookie{comment = Value}); -cookie_attributes([{"domain", Value}| Attributes], Cookie) -> - cookie_attributes(Attributes, - Cookie#http_cookie{domain = Value}); -cookie_attributes([{"max-age", Value}| Attributes], Cookie) -> - ExpireTime = cookie_expires(list_to_integer(Value)), - cookie_attributes(Attributes, - Cookie#http_cookie{max_age = ExpireTime}); -%% Backwards compatibility with netscape cookies -cookie_attributes([{"expires", Value}| Attributes], Cookie) -> - Time = http_util:convert_netscapecookie_date(Value), - ExpireTime = calendar:datetime_to_gregorian_seconds(Time), - cookie_attributes(Attributes, - Cookie#http_cookie{max_age = ExpireTime}); -cookie_attributes([{"path", Value}| Attributes], Cookie) -> - cookie_attributes(Attributes, - Cookie#http_cookie{path = Value}); -cookie_attributes([{"secure", _}| Attributes], Cookie) -> - cookie_attributes(Attributes, - Cookie#http_cookie{secure = true}); -cookie_attributes([{"version", Value}| Attributes], Cookie) -> - cookie_attributes(Attributes, - Cookie#http_cookie{version = Value}); -%% Disregard unknown attributes. -cookie_attributes([_| Attributes], Cookie) -> - cookie_attributes(Attributes, Cookie). - -domain_default(Cookie = #http_cookie{domain = undefined}, - DefaultDomain) -> - Cookie#http_cookie{domain = DefaultDomain, domain_default = true}; -domain_default(Cookie, _) -> - Cookie. - -path_default(Cookie = #http_cookie{path = undefined}, - DefaultPath) -> - Cookie#http_cookie{path = skip_right_most_slash(DefaultPath), - path_default = true}; -path_default(Cookie, _) -> - Cookie. - -%% Note: if the path is only / that / will be keept -skip_right_most_slash("/") -> - "/"; -skip_right_most_slash(Str) -> - string:strip(Str, right, $/). - -accept_cookies(Cookies, RequestPath, RequestHost) -> - lists:filter(fun(Cookie) -> - accept_cookie(Cookie, RequestPath, RequestHost) - end, Cookies). - -accept_cookie(Cookie, RequestPath, RequestHost) -> - accept_path(Cookie, RequestPath) and accept_domain(Cookie, RequestHost). - -accept_path(#http_cookie{path = Path}, RequestPath) -> - lists:prefix(Path, RequestPath). - -accept_domain(#http_cookie{domain = RequestHost}, RequestHost) -> - true; - -accept_domain(#http_cookie{domain = Domain}, RequestHost) -> - HostCheck = case http_util:is_hostname(RequestHost) of - true -> - (lists:suffix(Domain, RequestHost) andalso - (not - lists:member($., - string:substr(RequestHost, 1, - (length(RequestHost) - - length(Domain)))))); - false -> - false - end, - HostCheck andalso (hd(Domain) == $.) - andalso (length(string:tokens(Domain, ".")) > 1). - -cookie_expires(0) -> - 0; -cookie_expires(DeltaSec) -> - NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}), - NowSec + DeltaSec. - -is_cookie_expired(#http_cookie{max_age = session}) -> - false; -is_cookie_expired(#http_cookie{max_age = ExpireTime}) -> - NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}), - ExpireTime - NowSec =< 0. - -valid_cookies([], Valid, _) -> - Valid; - -valid_cookies([Cookie | Cookies], Valid, Db) -> - case is_cookie_expired(Cookie) of - true -> - delete(Cookie, Db), - valid_cookies(Cookies, Valid, Db); - false -> - valid_cookies(Cookies, [Cookie | Valid], Db) - end. - -path_sort(Cookies)-> - lists:reverse(lists:keysort(#http_cookie.path, Cookies)). - - -%% Informally, the Set-Cookie response header comprises the token -%% Set-Cookie:, followed by a comma-separated list of one or more -%% cookies. Netscape cookies expires attribute may also have a -%% , in this case the header list will have been incorrectly split -%% in parse_set_cookies/2 this functions fixs that problem. -fix_netscape_cookie([Cookie1, Cookie2 | Rest], Acc) -> - case inets_regexp:match(Cookie1, "expires=") of - {_, _, _} -> - fix_netscape_cookie(Rest, [Cookie1 ++ Cookie2 | Acc]); - nomatch -> - fix_netscape_cookie([Cookie2 |Rest], [Cookie1| Acc]) - end; -fix_netscape_cookie([Cookie | Rest], Acc) -> - fix_netscape_cookie(Rest, [Cookie | Acc]); - -fix_netscape_cookie([], Acc) -> - Acc. diff --git a/lib/inets/src/http_client/httpc.erl b/lib/inets/src/http_client/httpc.erl new file mode 100644 index 0000000000..c4ee4f1fda --- /dev/null +++ b/lib/inets/src/http_client/httpc.erl @@ -0,0 +1,1030 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% + +%% Description: +%%% This version of the HTTP/1.1 client supports: +%%% - RFC 2616 HTTP 1.1 client part +%%% - RFC 2818 HTTP Over TLS + +-module(httpc). + +-behaviour(inets_service). + +%% API +-export([request/1, request/2, request/4, request/5, + cancel_request/1, cancel_request/2, + set_option/2, set_option/3, + set_options/1, set_options/2, + store_cookies/2, store_cookies/3, + cookie_header/1, cookie_header/2, + which_cookies/0, which_cookies/1, + reset_cookies/0, reset_cookies/1, + stream_next/1, + default_profile/0, + profile_name/1, profile_name/2]). + +%% Behavior callbacks +-export([start_standalone/1, start_service/1, + stop_service/1, + services/0, service_info/1]). + +-include("http_internal.hrl"). +-include("httpc_internal.hrl"). + +-define(DEFAULT_PROFILE, default). + + +%%%========================================================================= +%%% API +%%%========================================================================= + +default_profile() -> + ?DEFAULT_PROFILE. + + +profile_name(?DEFAULT_PROFILE) -> + httpc_manager; +profile_name(Profile) -> + profile_name("httpc_manager_", Profile). + +profile_name(Prefix, Profile) when is_atom(Profile) -> + list_to_atom(Prefix ++ atom_to_list(Profile)); +profile_name(Prefix, Profile) when is_pid(Profile) -> + ProfileStr0 = + string:strip(string:strip(erlang:pid_to_list(Profile), left, $<), right, $>), + F = fun($.) -> $_; (X) -> X end, + ProfileStr = [F(C) || C <- ProfileStr0], + list_to_atom(Prefix ++ "pid_" ++ ProfileStr). + + +%%-------------------------------------------------------------------------- +%% request(Url) -> {ok, {StatusLine, Headers, Body}} | {error,Reason} +%% request(Url Profile) -> +%% {ok, {StatusLine, Headers, Body}} | {error,Reason} +%% +%% Url - string() +%% Description: Calls request/4 with default values. +%%-------------------------------------------------------------------------- + +request(Url) -> + request(Url, default_profile()). + +request(Url, Profile) -> + request(get, {Url, []}, [], [], Profile). + + +%%-------------------------------------------------------------------------- +%% request(Method, Request, HTTPOptions, Options [, Profile]) -> +%% {ok, {StatusLine, Headers, Body}} | {ok, {Status, Body}} | +%% {ok, RequestId} | {error,Reason} | {ok, {saved_as, FilePath} +%% +%% Method - atom() = head | get | put | post | trace | options| delete +%% Request - {Url, Headers} | {Url, Headers, ContentType, Body} +%% Url - string() +%% HTTPOptions - [HttpOption] +%% HTTPOption - {timeout, Time} | {connect_timeout, Time} | +%% {ssl, SSLOptions} | {proxy_auth, {User, Password}} +%% Ssloptions = [SSLOption] +%% SSLOption = {verify, code()} | {depth, depth()} | {certfile, path()} | +%% {keyfile, path()} | {password, string()} | {cacertfile, path()} | +%% {ciphers, string()} +%% Options - [Option] +%% Option - {sync, Boolean} | {body_format, BodyFormat} | +%% {full_result, Boolean} | {stream, To} | +%% {headers_as_is, Boolean} +%% StatusLine = {HTTPVersion, StatusCode, ReasonPhrase} +%% HTTPVersion = string() +%% StatusCode = integer() +%% ReasonPhrase = string() +%% Headers = [Header] +%% Header = {Field, Value} +%% Field = string() +%% Value = string() +%% Body = string() | binary() - HTLM-code +%% +%% Description: Sends a HTTP-request. The function can be both +%% syncronus and asynchronous in the later case the function will +%% return {ok, RequestId} and later on a message will be sent to the +%% calling process on the format {http, {RequestId, {StatusLine, +%% Headers, Body}}} or {http, {RequestId, {error, Reason}}} +%%-------------------------------------------------------------------------- + +request(Method, Request, HttpOptions, Options) -> + request(Method, Request, HttpOptions, Options, default_profile()). + +request(Method, {Url, Headers}, HTTPOptions, Options, Profile) + when (Method =:= options) orelse + (Method =:= get) orelse + (Method =:= head) orelse + (Method =:= delete) orelse + (Method =:= trace) andalso + (is_atom(Profile) orelse is_pid(Profile)) -> + ?hcrt("request", [{method, Method}, + {url, Url}, + {headers, Headers}, + {http_options, HTTPOptions}, + {options, Options}, + {profile, Profile}]), + case http_uri:parse(Url) of + {error, Reason} -> + {error, Reason}; + ParsedUrl -> + handle_request(Method, Url, ParsedUrl, Headers, [], [], + HTTPOptions, Options, Profile) + end; + +request(Method, {Url,Headers,ContentType,Body}, HTTPOptions, Options, Profile) + when ((Method =:= post) orelse (Method =:= put)) andalso + (is_atom(Profile) orelse is_pid(Profile)) -> + ?hcrt("request", [{method, Method}, + {url, Url}, + {headers, Headers}, + {content_type, ContentType}, + {body, Body}, + {http_options, HTTPOptions}, + {options, Options}, + {profile, Profile}]), + case http_uri:parse(Url) of + {error, Reason} -> + {error, Reason}; + ParsedUrl -> + handle_request(Method, Url, + ParsedUrl, Headers, ContentType, Body, + HTTPOptions, Options, Profile) + end. + + +%%-------------------------------------------------------------------------- +%% cancel_request(RequestId) -> ok +%% cancel_request(RequestId, Profile) -> ok +%% RequestId - As returned by request/4 +%% +%% Description: Cancels a HTTP-request. +%%------------------------------------------------------------------------- +cancel_request(RequestId) -> + cancel_request(RequestId, default_profile()). + +cancel_request(RequestId, Profile) + when is_atom(Profile) orelse is_pid(Profile) -> + ?hcrt("cancel request", [{request_id, RequestId}, {profile, Profile}]), + ok = httpc_manager:cancel_request(RequestId, profile_name(Profile)), + receive + %% If the request was already fulfilled throw away the + %% answer as the request has been canceled. + {http, {RequestId, _}} -> + ok + after 0 -> + ok + end. + + +%%-------------------------------------------------------------------------- +%% set_options(Options) -> ok | {error, Reason} +%% set_options(Options, Profile) -> ok | {error, Reason} +%% Options - [Option] +%% Profile - atom() +%% Option - {proxy, {Proxy, NoProxy}} | {max_sessions, MaxSessions} | +%% {max_pipeline_length, MaxPipeline} | +%% {pipeline_timeout, PipelineTimeout} | {cookies, CookieMode} | +%% {ipfamily, IpFamily} +%% Proxy - {Host, Port} +%% NoProxy - [Domain | HostName | IPAddress] +%% MaxSessions, MaxPipeline, PipelineTimeout = integer() +%% CookieMode - enabled | disabled | verify +%% IpFamily - inet | inet6 | inet6fb4 +%% Description: Informs the httpc_manager of the new settings. +%%------------------------------------------------------------------------- +set_options(Options) -> + set_options(Options, default_profile()). +set_options(Options, Profile) when is_atom(Profile) orelse is_pid(Profile) -> + ?hcrt("set cookies", [{options, Options}, {profile, Profile}]), + case validate_options(Options) of + {ok, Opts} -> + try + begin + httpc_manager:set_options(Opts, profile_name(Profile)) + end + catch + exit:{noproc, _} -> + {error, inets_not_started} + end; + {error, Reason} -> + {error, Reason} + end. + +set_option(Key, Value) -> + set_option(Key, Value, default_profile()). + +set_option(Key, Value, Profile) -> + set_options([{Key, Value}], Profile). + + +%%-------------------------------------------------------------------------- +%% store_cookies(SetCookieHeaders, Url [, Profile]) -> ok | {error, reason} +%% +%% +%% Description: Store the cookies from +%% in the cookie database +%% for the profile . This function shall be used when the option +%% cookie is set to verify. +%%------------------------------------------------------------------------- +store_cookies(SetCookieHeaders, Url) -> + store_cookies(SetCookieHeaders, Url, default_profile()). + +store_cookies(SetCookieHeaders, Url, Profile) + when is_atom(Profile) orelse is_pid(Profile) -> + ?hcrt("store cookies", [{set_cookie_headers, SetCookieHeaders}, + {url, Url}, + {profile, Profile}]), + try + begin + {_, _, Host, Port, Path, _} = http_uri:parse(Url), + Address = {Host, Port}, + ProfileName = profile_name(Profile), + Cookies = httpc_cookie:cookies(SetCookieHeaders, Path, Host), + httpc_manager:store_cookies(Cookies, Address, ProfileName), + ok + end + catch + exit:{noproc, _} -> + {error, {not_started, Profile}}; + error:{badmatch, Bad} -> + {error, {parse_failed, Bad}} + end. + + +%%-------------------------------------------------------------------------- +%% cookie_header(Url [, Profile]) -> Header | {error, Reason} +%% +%% Description: Returns the cookie header that would be sent when making +%% a request to . +%%------------------------------------------------------------------------- +cookie_header(Url) -> + cookie_header(Url, default_profile()). + +cookie_header(Url, Profile) -> + ?hcrt("cookie header", [{url, Url}, + {profile, Profile}]), + try + begin + httpc_manager:which_cookies(Url, profile_name(Profile)) + end + catch + exit:{noproc, _} -> + {error, {not_started, Profile}} + end. + + +%%-------------------------------------------------------------------------- +%% which_cookies() -> [cookie()] +%% which_cookies(Profile) -> [cookie()] +%% +%% Description: Debug function, dumping the cookie database +%%------------------------------------------------------------------------- +which_cookies() -> + which_cookies(default_profile()). + +which_cookies(Profile) -> + ?hcrt("which cookies", [{profile, Profile}]), + try + begin + httpc_manager:which_cookies(profile_name(Profile)) + end + catch + exit:{noproc, _} -> + {error, {not_started, Profile}} + end. + + +%%-------------------------------------------------------------------------- +%% reset_cookies() -> void() +%% reset_cookies(Profile) -> void() +%% +%% Description: Debug function, reset the cookie database +%%------------------------------------------------------------------------- +reset_cookies() -> + reset_cookies(default_profile()). + +reset_cookies(Profile) -> + ?hcrt("reset cookies", [{profile, Profile}]), + try + begin + httpc_manager:reset_cookies(profile_name(Profile)) + end + catch + exit:{noproc, _} -> + {error, {not_started, Profile}} + end. + + +%%-------------------------------------------------------------------------- +%% stream_next(Pid) -> Header | {error, Reason} +%% +%% Description: Triggers the next message to be streamed, e.i. +%% same behavior as active once for sockets. +%%------------------------------------------------------------------------- +stream_next(Pid) -> + ?hcrt("stream next", [{handler, Pid}]), + httpc_handler:stream_next(Pid). + + +%%%======================================================================== +%%% Behaviour callbacks +%%%======================================================================== +start_standalone(PropList) -> + ?hcrt("start standalone", [{proplist, PropList}]), + case proplists:get_value(profile, PropList) of + undefined -> + {error, no_profile}; + Profile -> + Dir = + proplists:get_value(data_dir, PropList, only_session_cookies), + httpc_manager:start_link(Profile, Dir, stand_alone) + end. + +start_service(Config) -> + ?hcrt("start service", [{config, Config}]), + httpc_profile_sup:start_child(Config). + +stop_service(Profile) when is_atom(Profile) -> + ?hcrt("stop service", [{profile, Profile}]), + httpc_profile_sup:stop_child(Profile); +stop_service(Pid) when is_pid(Pid) -> + ?hcrt("stop service", [{pid, Pid}]), + case service_info(Pid) of + {ok, [{profile, Profile}]} -> + stop_service(Profile); + Error -> + Error + end. + +services() -> + [{httpc, Pid} || {_, Pid, _, _} <- + supervisor:which_children(httpc_profile_sup)]. +service_info(Pid) -> + try [{ChildName, ChildPid} || + {ChildName, ChildPid, _, _} <- + supervisor:which_children(httpc_profile_sup)] of + Children -> + child_name2info(child_name(Pid, Children)) + catch + exit:{noproc, _} -> + {error, service_not_available} + end. + + +%%%======================================================================== +%%% Internal functions +%%%======================================================================== + +handle_request(Method, Url, + {Scheme, UserInfo, Host, Port, Path, Query}, + Headers, ContentType, Body, + HTTPOptions0, Options0, Profile) -> + + Started = http_util:timestamp(), + NewHeaders = [{http_util:to_lower(Key), Val} || {Key, Val} <- Headers], + + try + begin + HTTPOptions = http_options(HTTPOptions0), + Options = request_options(Options0), + Sync = proplists:get_value(sync, Options), + Stream = proplists:get_value(stream, Options), + HeadersRecord = + header_record(NewHeaders, + #http_request_h{}, + header_host(Host, Port), + HTTPOptions#http_options.version), + Receiver = proplists:get_value(receiver, Options), + Request = #request{from = Receiver, + scheme = Scheme, + address = {Host,Port}, + path = Path, + pquery = Query, + method = Method, + headers = HeadersRecord, + content = {ContentType,Body}, + settings = HTTPOptions, + abs_uri = Url, + userinfo = UserInfo, + stream = Stream, + headers_as_is = headers_as_is(Headers, Options), + started = Started}, + case httpc_manager:request(Request, profile_name(Profile)) of + {ok, RequestId} -> + handle_answer(RequestId, Sync, Options); + {error, Reason} -> + {error, Reason} + end + end + catch + error:{noproc, _} -> + {error, {not_started, Profile}}; + throw:Error -> + Error + end. + + +handle_answer(RequestId, false, _) -> + {ok, RequestId}; +handle_answer(RequestId, true, Options) -> + receive + {http, {RequestId, saved_to_file}} -> + {ok, saved_to_file}; + {http, {RequestId, {_,_,_} = Result}} -> + return_answer(Options, Result); + {http, {RequestId, {error, Reason}}} -> + {error, Reason} + end. + +return_answer(Options, {{"HTTP/0.9",_,_}, _, BinBody}) -> + Body = maybe_format_body(BinBody, Options), + {ok, Body}; + +return_answer(Options, {StatusLine, Headers, BinBody}) -> + + Body = maybe_format_body(BinBody, Options), + + case proplists:get_value(full_result, Options, true) of + true -> + {ok, {StatusLine, Headers, Body}}; + false -> + {_, Status, _} = StatusLine, + {ok, {Status, Body}} + end. + +maybe_format_body(BinBody, Options) -> + case proplists:get_value(body_format, Options, string) of + string -> + binary_to_list(BinBody); + _ -> + BinBody + end. + +%% This options is a workaround for http servers that do not follow the +%% http standard and have case sensative header parsing. Should only be +%% used if there is no other way to communicate with the server or for +%% testing purpose. +headers_as_is(Headers, Options) -> + case proplists:get_value(headers_as_is, Options, false) of + false -> + []; + true -> + Headers + end. + + +http_options(HttpOptions) -> + HttpOptionsDefault = http_options_default(), + http_options(HttpOptionsDefault, HttpOptions, #http_options{}). + +http_options([], [], Acc) -> + Acc; +http_options([], HttpOptions, Acc) -> + Fun = fun(BadOption) -> + Report = io_lib:format("Invalid option ~p ignored ~n", + [BadOption]), + error_logger:info_report(Report) + end, + lists:foreach(Fun, HttpOptions), + Acc; +http_options([{Tag, Default, Idx, Post} | Defaults], HttpOptions, Acc) -> + case lists:keysearch(Tag, 1, HttpOptions) of + {value, {Tag, Val0}} -> + case Post(Val0) of + {ok, Val} -> + Acc2 = setelement(Idx, Acc, Val), + HttpOptions2 = lists:keydelete(Tag, 1, HttpOptions), + http_options(Defaults, HttpOptions2, Acc2); + error -> + Report = io_lib:format("Invalid option ~p:~p ignored ~n", + [Tag, Val0]), + error_logger:info_report(Report), + HttpOptions2 = lists:keydelete(Tag, 1, HttpOptions), + http_options(Defaults, HttpOptions2, Acc) + end; + false -> + DefaultVal = + case Default of + {value, Val} -> + Val; + {field, DefaultIdx} -> + element(DefaultIdx, Acc) + end, + Acc2 = setelement(Idx, Acc, DefaultVal), + http_options(Defaults, HttpOptions, Acc2) + end. + +http_options_default() -> + VersionPost = + fun(Value) when is_atom(Value) -> + {ok, http_util:to_upper(atom_to_list(Value))}; + (Value) when is_list(Value) -> + {ok, http_util:to_upper(Value)}; + (_) -> + error + end, + TimeoutPost = fun(Value) when is_integer(Value) andalso (Value >= 0) -> + {ok, Value}; + (infinity = Value) -> + {ok, Value}; + (_) -> + error + end, + AutoRedirectPost = fun(Value) when (Value =:= true) orelse + (Value =:= false) -> + {ok, Value}; + (_) -> + error + end, + SslPost = fun(Value) when is_list(Value) -> + {ok, Value}; + (_) -> + error + end, + ProxyAuthPost = fun({User, Passwd} = Value) when is_list(User) andalso + is_list(Passwd) -> + {ok, Value}; + (_) -> + error + end, + RelaxedPost = fun(Value) when (Value =:= true) orelse + (Value =:= false) -> + {ok, Value}; + (_) -> + error + end, + ConnTimeoutPost = + fun(Value) when is_integer(Value) andalso (Value >= 0) -> + {ok, Value}; + (infinity = Value) -> + {ok, Value}; + (_) -> + error + end, + [ + {version, {value, "HTTP/1.1"}, #http_options.version, VersionPost}, + {timeout, {value, ?HTTP_REQUEST_TIMEOUT}, #http_options.timeout, TimeoutPost}, + {autoredirect, {value, true}, #http_options.autoredirect, AutoRedirectPost}, + {ssl, {value, []}, #http_options.ssl, SslPost}, + {proxy_auth, {value, undefined}, #http_options.proxy_auth, ProxyAuthPost}, + {relaxed, {value, false}, #http_options.relaxed, RelaxedPost}, + %% this field has to be *after* the timeout field (as that field is used for the default value) + {connect_timeout, {field, #http_options.timeout}, #http_options.connect_timeout, ConnTimeoutPost} + ]. + +request_options_defaults() -> + VerifyBoolean = + fun(Value) when ((Value =:= true) orelse (Value =:= false)) -> + ok; + (_) -> + error + end, + + VerifySync = VerifyBoolean, + + VerifyStream = + fun(none = _Value) -> + ok; + (self = _Value) -> + ok; + ({self, once} = _Value) -> + ok; + (Value) when is_list(Value) -> + ok; + (_) -> + error + end, + + VerifyBodyFormat = + fun(string = _Value) -> + ok; + (binary = _Value) -> + ok; + (_) -> + error + end, + + VerifyFullResult = VerifyBoolean, + + VerifyHeaderAsIs = VerifyBoolean, + + VerifyReceiver = + fun(Value) when is_pid(Value) -> + ok; + ({M, F, A}) when (is_atom(M) andalso + is_atom(F) andalso + is_list(A)) -> + ok; + (Value) when is_function(Value, 1) -> + ok; + (_) -> + error + end, + + [ + {sync, true, VerifySync}, + {stream, none, VerifyStream}, + {body_format, string, VerifyBodyFormat}, + {full_result, true, VerifyFullResult}, + {headers_as_is, false, VerifyHeaderAsIs}, + {receiver, self(), VerifyReceiver} + ]. + +request_options(Options) -> + Defaults = request_options_defaults(), + request_options(Defaults, Options, []). + +request_options([], [], Acc) -> + request_options_sanity_check(Acc), + lists:reverse(Acc); +request_options([], Options, Acc) -> + Fun = fun(BadOption) -> + Report = io_lib:format("Invalid option ~p ignored ~n", + [BadOption]), + error_logger:info_report(Report) + end, + lists:foreach(Fun, Options), + Acc; +request_options([{Key, DefaultVal, Verify} | Defaults], Options, Acc) -> + case lists:keysearch(Key, 1, Options) of + {value, {Key, Value}} -> + case Verify(Value) of + ok -> + Options2 = lists:keydelete(Key, 1, Options), + request_options(Defaults, Options2, [{Key, Value} | Acc]); + error -> + Report = io_lib:format("Invalid option ~p:~p ignored ~n", + [Key, Value]), + error_logger:info_report(Report), + Options2 = lists:keydelete(Key, 1, Options), + request_options(Defaults, Options2, Acc) + end; + false -> + request_options(Defaults, Options, [{Key, DefaultVal} | Acc]) + end. + +request_options_sanity_check(Opts) -> + case proplists:get_value(sync, Opts) of + Sync when (Sync =:= true) -> + case proplists:get_value(receiver, Opts) of + Pid when is_pid(Pid) andalso (Pid =:= self()) -> + ok; + BadReceiver -> + throw({error, {bad_options_combo, + [{sync, true}, {receiver, BadReceiver}]}}) + end, + case proplists:get_value(stream, Opts) of + Stream when (Stream =:= self) orelse + (Stream =:= {self, once}) -> + throw({error, streaming_error}); + _ -> + ok + end; + _ -> + ok + end, + ok. + +validate_options(Options) -> + (catch validate_options(Options, [])). + +validate_options([], ValidateOptions) -> + {ok, lists:reverse(ValidateOptions)}; + +validate_options([{proxy, Proxy} = Opt| Tail], Acc) -> + validate_proxy(Proxy), + validate_options(Tail, [Opt | Acc]); + +validate_options([{max_sessions, Value} = Opt| Tail], Acc) -> + validate_max_sessions(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{keep_alive_timeout, Value} = Opt| Tail], Acc) -> + validate_keep_alive_timeout(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{max_keep_alive_length, Value} = Opt| Tail], Acc) -> + validate_max_keep_alive_length(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{pipeline_timeout, Value} = Opt| Tail], Acc) -> + validate_pipeline_timeout(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{max_pipeline_length, Value} = Opt| Tail], Acc) -> + validate_max_pipeline_length(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{cookies, Value} = Opt| Tail], Acc) -> + validate_cookies(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{ipfamily, Value} = Opt| Tail], Acc) -> + validate_ipfamily(Value), + validate_options(Tail, [Opt | Acc]); + +%% For backward compatibillity +validate_options([{ipv6, Value}| Tail], Acc) -> + NewValue = validate_ipv6(Value), + Opt = {ipfamily, NewValue}, + validate_options(Tail, [Opt | Acc]); + +validate_options([{ip, Value} = Opt| Tail], Acc) -> + validate_ip(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{port, Value} = Opt| Tail], Acc) -> + validate_port(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{verbose, Value} = Opt| Tail], Acc) -> + validate_verbose(Value), + validate_options(Tail, [Opt | Acc]); + +validate_options([{_, _} = Opt| _], _Acc) -> + {error, {not_an_option, Opt}}. + + +validate_proxy({{ProxyHost, ProxyPort}, NoProxy} = Proxy) + when is_list(ProxyHost) andalso + is_integer(ProxyPort) andalso + is_list(NoProxy) -> + Proxy; +validate_proxy(BadProxy) -> + bad_option(proxy, BadProxy). + +validate_max_sessions(Value) when is_integer(Value) andalso (Value >= 0) -> + Value; +validate_max_sessions(BadValue) -> + bad_option(max_sessions, BadValue). + +validate_keep_alive_timeout(Value) when is_integer(Value) andalso (Value >= 0) -> + Value; +validate_keep_alive_timeout(infinity = Value) -> + Value; +validate_keep_alive_timeout(BadValue) -> + bad_option(keep_alive_timeout, BadValue). + +validate_max_keep_alive_length(Value) when is_integer(Value) andalso (Value >= 0) -> + Value; +validate_max_keep_alive_length(BadValue) -> + bad_option(max_keep_alive_length, BadValue). + +validate_pipeline_timeout(Value) when is_integer(Value) -> + Value; +validate_pipeline_timeout(infinity = Value) -> + Value; +validate_pipeline_timeout(BadValue) -> + bad_option(pipeline_timeout, BadValue). + +validate_max_pipeline_length(Value) when is_integer(Value) -> + Value; +validate_max_pipeline_length(BadValue) -> + bad_option(max_pipeline_length, BadValue). + +validate_cookies(Value) + when ((Value =:= enabled) orelse + (Value =:= disabled) orelse + (Value =:= verify)) -> + Value; +validate_cookies(BadValue) -> + bad_option(cookies, BadValue). + +validate_ipv6(Value) when (Value =:= enabled) orelse (Value =:= disabled) -> + case Value of + enabled -> + inet6fb4; + disabled -> + inet + end; +validate_ipv6(BadValue) -> + bad_option(ipv6, BadValue). + +validate_ipfamily(Value) + when (Value =:= inet) orelse (Value =:= inet6) orelse (Value =:= inet6fb4) -> + Value; +validate_ipfamily(BadValue) -> + bad_option(ipfamily, BadValue). + +validate_ip(Value) + when is_tuple(Value) andalso ((size(Value) =:= 4) orelse (size(Value) =:= 8)) -> + Value; +validate_ip(BadValue) -> + bad_option(ip, BadValue). + +validate_port(Value) when is_integer(Value) -> + Value; +validate_port(BadValue) -> + bad_option(port, BadValue). + +validate_verbose(Value) + when ((Value =:= false) orelse + (Value =:= verbose) orelse + (Value =:= debug) orelse + (Value =:= trace)) -> + ok; +validate_verbose(BadValue) -> + bad_option(verbose, BadValue). + +bad_option(Option, BadValue) -> + throw({error, {bad_option, Option, BadValue}}). + + +header_host(Host, 80 = _Port) -> + Host; +header_host(Host, Port) -> + Host ++ ":" ++ integer_to_list(Port). + + +header_record([], RequestHeaders, Host, Version) -> + validate_headers(RequestHeaders, Host, Version); +header_record([{"cache-control", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'cache-control' = Val}, + Host, Version); +header_record([{"connection", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{connection = Val}, Host, + Version); +header_record([{"date", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{date = Val}, Host, + Version); +header_record([{"pragma", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{pragma = Val}, Host, + Version); +header_record([{"trailer", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{trailer = Val}, Host, + Version); +header_record([{"transfer-encoding", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, + RequestHeaders#http_request_h{'transfer-encoding' = Val}, + Host, Version); +header_record([{"upgrade", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{upgrade = Val}, Host, + Version); +header_record([{"via", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{via = Val}, Host, + Version); +header_record([{"warning", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{warning = Val}, Host, + Version); +header_record([{"accept", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{accept = Val}, Host, + Version); +header_record([{"accept-charset", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'accept-charset' = Val}, + Host, Version); +header_record([{"accept-encoding", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, RequestHeaders#http_request_h{'accept-encoding' = Val}, + Host, Version); +header_record([{"accept-language", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, RequestHeaders#http_request_h{'accept-language' = Val}, + Host, Version); +header_record([{"authorization", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{authorization = Val}, + Host, Version); +header_record([{"expect", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{expect = Val}, Host, + Version); +header_record([{"from", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{from = Val}, Host, + Version); +header_record([{"host", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{host = Val}, Host, + Version); +header_record([{"if-match", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'if-match' = Val}, + Host, Version); +header_record([{"if-modified-since", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, + RequestHeaders#http_request_h{'if-modified-since' = Val}, + Host, Version); +header_record([{"if-none-match", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'if-none-match' = Val}, + Host, Version); +header_record([{"if-range", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'if-range' = Val}, + Host, Version); + +header_record([{"if-unmodified-since", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, RequestHeaders#http_request_h{'if-unmodified-since' + = Val}, Host, Version); +header_record([{"max-forwards", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'max-forwards' = Val}, + Host, Version); +header_record([{"proxy-authorization", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, RequestHeaders#http_request_h{'proxy-authorization' + = Val}, Host, Version); +header_record([{"range", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{range = Val}, Host, + Version); +header_record([{"referer", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{referer = Val}, Host, + Version); +header_record([{"te", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{te = Val}, Host, + Version); +header_record([{"user-agent", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'user-agent' = Val}, + Host, Version); +header_record([{"allow", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{allow = Val}, Host, + Version); +header_record([{"content-encoding", Val} | Rest], RequestHeaders, Host, + Version) -> + header_record(Rest, + RequestHeaders#http_request_h{'content-encoding' = Val}, + Host, Version); +header_record([{"content-language", Val} | Rest], RequestHeaders, + Host, Version) -> + header_record(Rest, + RequestHeaders#http_request_h{'content-language' = Val}, + Host, Version); +header_record([{"content-length", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'content-length' = Val}, + Host, Version); +header_record([{"content-location", Val} | Rest], RequestHeaders, + Host, Version) -> + header_record(Rest, + RequestHeaders#http_request_h{'content-location' = Val}, + Host, Version); +header_record([{"content-md5", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'content-md5' = Val}, + Host, Version); +header_record([{"content-range", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'content-range' = Val}, + Host, Version); +header_record([{"content-type", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'content-type' = Val}, + Host, Version); +header_record([{"expires", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{expires = Val}, Host, + Version); +header_record([{"last-modified", Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{'last-modified' = Val}, + Host, Version); +header_record([{Key, Val} | Rest], RequestHeaders, Host, Version) -> + header_record(Rest, RequestHeaders#http_request_h{ + other = [{Key, Val} | + RequestHeaders#http_request_h.other]}, + Host, Version). + +validate_headers(RequestHeaders = #http_request_h{te = undefined}, Host, + "HTTP/1.1" = Version) -> + validate_headers(RequestHeaders#http_request_h{te = ""}, Host, + "HTTP/1.1" = Version); +validate_headers(RequestHeaders = #http_request_h{host = undefined}, + Host, "HTTP/1.1" = Version) -> + validate_headers(RequestHeaders#http_request_h{host = Host}, Host, Version); +validate_headers(RequestHeaders, _, _) -> + RequestHeaders. + + +child_name2info(undefined) -> + {error, no_such_service}; +child_name2info(httpc_manager) -> + {ok, [{profile, default}]}; +child_name2info({httpc, Profile}) -> + {ok, [{profile, Profile}]}. + +child_name(_, []) -> + undefined; +child_name(Pid, [{Name, Pid} | _]) -> + Name; +child_name(Pid, [_ | Children]) -> + child_name(Pid, Children). + +%% d(F) -> +%% d(F, []). + +%% d(F, A) -> +%% d(get(dbg), F, A). + +%% d(true, F, A) -> +%% io:format(user, "~w:~w:" ++ F ++ "~n", [self(), ?MODULE | A]); +%% d(_, _, _) -> +%% ok. + diff --git a/lib/inets/src/http_client/httpc_cookie.erl b/lib/inets/src/http_client/httpc_cookie.erl new file mode 100644 index 0000000000..586701b4a1 --- /dev/null +++ b/lib/inets/src/http_client/httpc_cookie.erl @@ -0,0 +1,495 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% Description: Cookie handling according to RFC 2109 + +-module(httpc_cookie). + +-include("httpc_internal.hrl"). + +-export([open_db/3, close_db/1, insert/2, header/4, cookies/3]). +-export([reset_db/1, which_cookies/1]). + +-record(cookie_db, {db, session_db}). + + +%%%========================================================================= +%%% API +%%%========================================================================= + +%%-------------------------------------------------------------------- +%% Func: open_db(DbName, DbDir, SessionDbName) -> #cookie_db{} +%% Purpose: Create the cookie db +%%-------------------------------------------------------------------- + +open_db(_, only_session_cookies, SessionDbName) -> + ?hcrt("open (session cookies only) db", + [{session_db_name, SessionDbName}]), + SessionDb = ets:new(SessionDbName, + [protected, bag, {keypos, #http_cookie.domain}]), + #cookie_db{session_db = SessionDb}; + +open_db(Name, Dir, SessionDbName) -> + ?hcrt("open db", + [{name, Name}, {dir, Dir}, {session_db_name, SessionDbName}]), + File = filename:join(Dir, atom_to_list(Name)), + case dets:open_file(Name, [{keypos, #http_cookie.domain}, + {type, bag}, + {file, File}, + {ram_file, true}]) of + {ok, Db} -> + SessionDb = ets:new(SessionDbName, + [protected, bag, + {keypos, #http_cookie.domain}]), + #cookie_db{db = Db, session_db = SessionDb}; + {error, Reason} -> + throw({error, {failed_open_file, Name, File, Reason}}) + end. + + +%%-------------------------------------------------------------------- +%% Func: reset_db(CookieDb) -> void() +%% Purpose: Reset (empty) the cookie database +%% +%%-------------------------------------------------------------------- + +reset_db(#cookie_db{db = undefined, session_db = SessionDb}) -> + ets:delete_all_objects(SessionDb), + ok; +reset_db(#cookie_db{db = Db, session_db = SessionDb}) -> + dets:delete_all_objects(Db), + ets:delete_all_objects(SessionDb), + ok. + + +%%-------------------------------------------------------------------- +%% Func: close_db(CookieDb) -> ok +%% Purpose: Close the cookie db +%%-------------------------------------------------------------------- + +close_db(#cookie_db{db = Db, session_db = SessionDb}) -> + ?hcrt("close db", []), + maybe_dets_close(Db), + ets:delete(SessionDb), + ok. + +maybe_dets_close(undefined) -> + ok; +maybe_dets_close(Db) -> + dets:close(Db). + + +%%-------------------------------------------------------------------- +%% Func: insert(CookieDb) -> ok +%% Purpose: Close the cookie db +%%-------------------------------------------------------------------- + +%% If no persistent cookie database is defined we +%% treat all cookies as if they where session cookies. +insert(#cookie_db{db = undefined} = CookieDb, + #http_cookie{max_age = Int} = Cookie) when is_integer(Int) -> + insert(CookieDb, Cookie#http_cookie{max_age = session}); + +insert(#cookie_db{session_db = SessionDb} = CookieDb, + #http_cookie{domain = Key, + name = Name, + path = Path, + max_age = session} = Cookie) -> + ?hcrt("insert session cookie", [{cookie, Cookie}]), + Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}, + case ets:match_object(SessionDb, Pattern) of + [] -> + ets:insert(SessionDb, Cookie); + [NewCookie] -> + delete(CookieDb, NewCookie), + ets:insert(SessionDb, Cookie) + end, + ok; +insert(#cookie_db{db = Db} = CookieDb, + #http_cookie{domain = Key, + name = Name, + path = Path, + max_age = 0}) -> + ?hcrt("insert", [{domain, Key}, {name, Name}, {path, Path}]), + Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}, + case dets:match_object(Db, Pattern) of + [] -> + ok; + [NewCookie] -> + delete(CookieDb, NewCookie) + end, + ok; +insert(#cookie_db{db = Db} = CookieDb, + #http_cookie{domain = Key, name = Name, path = Path} = Cookie) -> + ?hcrt("insert", [{cookie, Cookie}]), + Pattern = #http_cookie{domain = Key, + name = Name, + path = Path, + _ = '_'}, + case dets:match_object(Db, Pattern) of + [] -> + dets:insert(Db, Cookie); + [OldCookie] -> + delete(CookieDb, OldCookie), + dets:insert(Db, Cookie) + end, + ok. + + + +%%-------------------------------------------------------------------- +%% Func: header(CookieDb) -> ok +%% Purpose: Cookies +%%-------------------------------------------------------------------- + +header(CookieDb, Scheme, {Host, _}, Path) -> + ?hcrd("header", [{scheme, Scheme}, {host, Host}, {path, Path}]), + case lookup_cookies(CookieDb, Host, Path) of + [] -> + {"cookie", ""}; + Cookies -> + {"cookie", cookies_to_string(Scheme, Cookies)} + end. + + +%%-------------------------------------------------------------------- +%% Func: cookies(Headers, RequestPath, RequestHost) -> [cookie()] +%% Purpose: Which cookies are stored +%%-------------------------------------------------------------------- + +cookies(Headers, RequestPath, RequestHost) -> + ?hcrt("cookies", [{headers, Headers}, + {request_path, RequestPath}, + {request_host, RequestHost}]), + Cookies = parse_set_cookies(Headers, {RequestPath, RequestHost}), + accept_cookies(Cookies, RequestPath, RequestHost). + + +%%-------------------------------------------------------------------- +%% Func: which_cookies(CookieDb) -> [cookie()] +%% Purpose: For test and debug purpose, +%% dump the entire cookie database +%%-------------------------------------------------------------------- + +which_cookies(#cookie_db{db = undefined, session_db = SessionDb}) -> + SessionCookies = ets:tab2list(SessionDb), + [{session_cookies, SessionCookies}]; +which_cookies(#cookie_db{db = Db, session_db = SessionDb}) -> + Cookies = dets:match_object(Db, '_'), + SessionCookies = ets:tab2list(SessionDb), + [{cookies, Cookies}, {session_cookies, SessionCookies}]. + + +%%%======================================================================== +%%% Internal functions +%%%======================================================================== + +delete(#cookie_db{session_db = SessionDb}, + #http_cookie{max_age = session} = Cookie) -> + ets:delete_object(SessionDb, Cookie); +delete(#cookie_db{db = Db}, Cookie) -> + dets:delete_object(Db, Cookie). + + +lookup_cookies(#cookie_db{db = undefined, session_db = SessionDb}, Key) -> + Pattern = #http_cookie{domain = Key, _ = '_'}, + Cookies = ets:match_object(SessionDb, Pattern), + ?hcrt("lookup cookies", [{cookies, Cookies}]), + Cookies; + +lookup_cookies(#cookie_db{db = Db, session_db = SessionDb}, Key) -> + Pattern = #http_cookie{domain = Key, _ = '_'}, + SessionCookies = ets:match_object(SessionDb, Pattern), + ?hcrt("lookup cookies", [{session_cookies, SessionCookies}]), + Cookies = dets:match_object(Db, Pattern), + ?hcrt("lookup cookies", [{cookies, Cookies}]), + Cookies ++ SessionCookies. + + +lookup_cookies(CookieDb, Host, Path) -> + Cookies = + case http_util:is_hostname(Host) of + true -> + HostCookies = lookup_cookies(CookieDb, Host), + [_| DomainParts] = string:tokens(Host, "."), + lookup_domain_cookies(CookieDb, DomainParts, HostCookies); + false -> % IP-adress + lookup_cookies(CookieDb, Host) + end, + ValidCookies = valid_cookies(CookieDb, Cookies), + lists:filter(fun(Cookie) -> + lists:prefix(Cookie#http_cookie.path, Path) + end, ValidCookies). + +%% For instance if Host=localhost +lookup_domain_cookies(_CookieDb, [], AccCookies) -> + lists:flatten(AccCookies); + +%% Top domains can not have cookies +lookup_domain_cookies(_CookieDb, [_], AccCookies) -> + lists:flatten(AccCookies); + +lookup_domain_cookies(CookieDb, [Next | DomainParts], AccCookies) -> + Domain = merge_domain_parts(DomainParts, [Next ++ "."]), + lookup_domain_cookies(CookieDb, DomainParts, + [lookup_cookies(CookieDb, Domain) | AccCookies]). + +merge_domain_parts([Part], Merged) -> + lists:flatten(["." | lists:reverse([Part | Merged])]); +merge_domain_parts([Part| Rest], Merged) -> + merge_domain_parts(Rest, [".", Part | Merged]). + +cookies_to_string(Scheme, [Cookie | _] = Cookies) -> + Version = "$Version=" ++ Cookie#http_cookie.version ++ "; ", + cookies_to_string(Scheme, path_sort(Cookies), [Version]). + +cookies_to_string(_, [], CookieStrs) -> + case length(CookieStrs) of + 1 -> + ""; + _ -> + lists:flatten(lists:reverse(CookieStrs)) + end; + +cookies_to_string(https, [#http_cookie{secure = true} = Cookie| Cookies], + CookieStrs) -> + Str = case Cookies of + [] -> + cookie_to_string(Cookie); + _ -> + cookie_to_string(Cookie) ++ "; " + end, + cookies_to_string(https, Cookies, [Str | CookieStrs]); + +cookies_to_string(Scheme, [#http_cookie{secure = true}| Cookies], + CookieStrs) -> + cookies_to_string(Scheme, Cookies, CookieStrs); + +cookies_to_string(Scheme, [Cookie | Cookies], CookieStrs) -> + Str = case Cookies of + [] -> + cookie_to_string(Cookie); + _ -> + cookie_to_string(Cookie) ++ "; " + end, + cookies_to_string(Scheme, Cookies, [Str | CookieStrs]). + +cookie_to_string(#http_cookie{name = Name, value = Value} = Cookie) -> + Str = Name ++ "=" ++ Value, + add_domain(add_path(Str, Cookie), Cookie). + +add_path(Str, #http_cookie{path_default = true}) -> + Str; +add_path(Str, #http_cookie{path = Path}) -> + Str ++ "; $Path=" ++ Path. + +add_domain(Str, #http_cookie{domain_default = true}) -> + Str; +add_domain(Str, #http_cookie{domain = Domain}) -> + Str ++ "; $Domain=" ++ Domain. + +parse_set_cookies(OtherHeaders, DefaultPathDomain) -> + SetCookieHeaders = + lists:foldl(fun({"set-cookie", Value}, Acc) -> + [string:tokens(Value, ",")| Acc]; + (_, Acc) -> + Acc + end, [], OtherHeaders), + + lists:flatten( + lists:map(fun(CookieHeader) -> + NewHeader = fix_netscape_cookie(CookieHeader, []), + parse_set_cookie(NewHeader, [], DefaultPathDomain) + end, + SetCookieHeaders)). + +parse_set_cookie([], AccCookies, _) -> + AccCookies; +parse_set_cookie([CookieHeader | CookieHeaders], AccCookies, + Defaults = {DefaultPath, DefaultDomain}) -> + [CookieStr | Attributes] = case string:tokens(CookieHeader, ";") of + [CStr] -> + [CStr, ""]; + [CStr | Attr] -> + [CStr, Attr] + end, + Pos = string:chr(CookieStr, $=), + Name = string:substr(CookieStr, 1, Pos - 1), + Value = string:substr(CookieStr, Pos + 1), + Cookie = #http_cookie{name = string:strip(Name), + value = string:strip(Value)}, + NewAttributes = parse_set_cookie_attributes(Attributes), + TmpCookie = cookie_attributes(NewAttributes, Cookie), + %% Add runtime defult values if necessary + NewCookie = domain_default(path_default(TmpCookie, DefaultPath), + DefaultDomain), + parse_set_cookie(CookieHeaders, [NewCookie | AccCookies], Defaults). + +parse_set_cookie_attributes([]) -> + []; +parse_set_cookie_attributes([Attributes]) -> + lists:map(fun(Attr) -> + [AttrName, AttrValue] = + case string:tokens(Attr, "=") of + %% All attributes have the form + %% Name=Value except "secure"! + [Name] -> + [Name, ""]; + [Name, Value] -> + [Name, Value]; + %% Anything not expected will be + %% disregarded + _ -> + ["Dummy",""] + end, + {http_util:to_lower(string:strip(AttrName)), + string:strip(AttrValue)} + end, Attributes). + +cookie_attributes([], Cookie) -> + Cookie; +cookie_attributes([{"comment", Value}| Attributes], Cookie) -> + cookie_attributes(Attributes, + Cookie#http_cookie{comment = Value}); +cookie_attributes([{"domain", Value}| Attributes], Cookie) -> + cookie_attributes(Attributes, + Cookie#http_cookie{domain = Value}); +cookie_attributes([{"max-age", Value}| Attributes], Cookie) -> + ExpireTime = cookie_expires(list_to_integer(Value)), + cookie_attributes(Attributes, + Cookie#http_cookie{max_age = ExpireTime}); +%% Backwards compatibility with netscape cookies +cookie_attributes([{"expires", Value}| Attributes], Cookie) -> + Time = http_util:convert_netscapecookie_date(Value), + ExpireTime = calendar:datetime_to_gregorian_seconds(Time), + cookie_attributes(Attributes, + Cookie#http_cookie{max_age = ExpireTime}); +cookie_attributes([{"path", Value}| Attributes], Cookie) -> + cookie_attributes(Attributes, + Cookie#http_cookie{path = Value}); +cookie_attributes([{"secure", _}| Attributes], Cookie) -> + cookie_attributes(Attributes, + Cookie#http_cookie{secure = true}); +cookie_attributes([{"version", Value}| Attributes], Cookie) -> + cookie_attributes(Attributes, + Cookie#http_cookie{version = Value}); +%% Disregard unknown attributes. +cookie_attributes([_| Attributes], Cookie) -> + cookie_attributes(Attributes, Cookie). + +domain_default(Cookie = #http_cookie{domain = undefined}, + DefaultDomain) -> + Cookie#http_cookie{domain = DefaultDomain, domain_default = true}; +domain_default(Cookie, _) -> + Cookie. + +path_default(#http_cookie{path = undefined} = Cookie, DefaultPath) -> + Cookie#http_cookie{path = skip_right_most_slash(DefaultPath), + path_default = true}; +path_default(Cookie, _) -> + Cookie. + +%% Note: if the path is only / that / will be keept +skip_right_most_slash("/") -> + "/"; +skip_right_most_slash(Str) -> + string:strip(Str, right, $/). + +accept_cookies(Cookies, RequestPath, RequestHost) -> + lists:filter(fun(Cookie) -> + accept_cookie(Cookie, RequestPath, RequestHost) + end, Cookies). + +accept_cookie(Cookie, RequestPath, RequestHost) -> + Accepted = + accept_path(Cookie, RequestPath) andalso + accept_domain(Cookie, RequestHost), + Accepted. + +accept_path(#http_cookie{path = Path}, RequestPath) -> + lists:prefix(Path, RequestPath). + +accept_domain(#http_cookie{domain = RequestHost}, RequestHost) -> + true; + +accept_domain(#http_cookie{domain = Domain}, RequestHost) -> + HostCheck = + case http_util:is_hostname(RequestHost) of + true -> + (lists:suffix(Domain, RequestHost) andalso + (not + lists:member($., + string:substr(RequestHost, 1, + (length(RequestHost) - + length(Domain)))))); + false -> + false + end, + HostCheck + andalso (hd(Domain) =:= $.) + andalso (length(string:tokens(Domain, ".")) > 1). + +cookie_expires(0) -> + 0; +cookie_expires(DeltaSec) -> + NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + NowSec + DeltaSec. + +is_cookie_expired(#http_cookie{max_age = session}) -> + false; +is_cookie_expired(#http_cookie{max_age = ExpireTime}) -> + NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + ExpireTime - NowSec =< 0. + + +valid_cookies(Db, Cookies) -> + valid_cookies(Db, Cookies, []). + +valid_cookies(_Db, [], Valid) -> + Valid; + +valid_cookies(Db, [Cookie | Cookies], Valid) -> + case is_cookie_expired(Cookie) of + true -> + delete(Db, Cookie), + valid_cookies(Db, Cookies, Valid); + false -> + valid_cookies(Db, Cookies, [Cookie | Valid]) + end. + +path_sort(Cookies)-> + lists:reverse(lists:keysort(#http_cookie.path, Cookies)). + + +%% Informally, the Set-Cookie response header comprises the token +%% Set-Cookie:, followed by a comma-separated list of one or more +%% cookies. Netscape cookies expires attribute may also have a +%% , in this case the header list will have been incorrectly split +%% in parse_set_cookies/2 this functions fixs that problem. +fix_netscape_cookie([Cookie1, Cookie2 | Rest], Acc) -> + case inets_regexp:match(Cookie1, "expires=") of + {_, _, _} -> + fix_netscape_cookie(Rest, [Cookie1 ++ Cookie2 | Acc]); + nomatch -> + fix_netscape_cookie([Cookie2 |Rest], [Cookie1| Acc]) + end; +fix_netscape_cookie([Cookie | Rest], Acc) -> + fix_netscape_cookie(Rest, [Cookie | Acc]); + +fix_netscape_cookie([], Acc) -> + Acc. diff --git a/lib/inets/src/http_client/httpc_handler.erl b/lib/inets/src/http_client/httpc_handler.erl index 7b737c2f86..25f9b0777f 100644 --- a/lib/inets/src/http_client/httpc_handler.erl +++ b/lib/inets/src/http_client/httpc_handler.erl @@ -1,19 +1,19 @@ %% %% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2002-2009. All Rights Reserved. -%% +%% +%% Copyright Ericsson AB 2002-2010. All Rights Reserved. +%% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. -%% +%% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. -%% +%% %% %CopyrightEnd% %% %% @@ -28,7 +28,8 @@ %%-------------------------------------------------------------------- %% Internal Application API --export([start_link/3, send/2, cancel/2, stream/3, stream_next/1]). +-export([start_link/2, connect_and_send/2, + send/2, cancel/2, stream/3, stream_next/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -50,7 +51,7 @@ mfa, % {Moduel, Function, Args} pipeline = queue:new(), % queue() keep_alive = queue:new(), % queue() - status = new, % new | pipeline | keep_alive | close | ssl_tunnel + status, % undefined | new | pipeline | keep_alive | close | ssl_tunnel canceled = [], % [RequestId] max_header_size = nolimit, % nolimit | integer() max_body_size = nolimit, % nolimit | integer() @@ -85,9 +86,13 @@ %% the reply or part of it has arrived.) %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- -start_link(Request, Options, ProfileName) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Request, Options, - ProfileName]])}. + +start_link(Options, ProfileName) -> + Args = [Options, ProfileName], + gen_server:start_link(?MODULE, Args, []). + +connect_and_send(Request, HandlerPid) -> + call({connect_and_send, Request}, HandlerPid). %%-------------------------------------------------------------------- @@ -192,10 +197,9 @@ stream(BodyPart, Request,_) -> % only 200 and 206 responses can be streamed %%==================================================================== %%-------------------------------------------------------------------- -%% Function: init([Request, Options, ProfileName]) -> {ok, State} | -%% {ok, State, Timeout} | ignore |{stop, Reason} +%% Function: init([Options, ProfileName]) -> {ok, State} | +%% {ok, State, Timeout} | ignore | {stop, Reason} %% -%% Request = #request{} %% Options = #options{} %% ProfileName = atom() - id of httpc manager process %% @@ -206,30 +210,16 @@ stream(BodyPart, Request,_) -> % only 200 and 206 responses can be streamed %% but we do not want that so errors will be handled by the process %% sending an init_error message to itself. %%-------------------------------------------------------------------- -init([Request, Options, ProfileName]) -> +init([Options, ProfileName]) -> + ?hcrv("init - starting", [{options, Options}, {profile, ProfileName}]), process_flag(trap_exit, true), - handle_verbose(Options#options.verbose), - Address = handle_proxy(Request#request.address, Options#options.proxy), - {ok, State} = - case {Address /= Request#request.address, Request#request.scheme} of - {true, https} -> - Error = https_through_proxy_is_not_currently_supported, - self() ! {init_error, - Error, httpc_response:error(Request, Error)}, - {ok, #state{request = Request, options = Options, - status = ssl_tunnel}}; - %% This is what we should do if and when ssl supports - %% "socket upgrading" - %%send_ssl_tunnel_request(Address, Request, - %% #state{options = Options, - %% status = ssl_tunnel}); - {_, _} -> - send_first_request(Address, Request, - #state{options = Options, - profile_name = ProfileName}) - end, - gen_server:enter_loop(?MODULE, [], State). + State = #state{status = undefined, + options = Options, + profile_name = ProfileName}, + ?hcrd("init - started", []), + {ok, State}. + %%-------------------------------------------------------------------- %% Function: handle_call(Request, From, State) -> {reply, Reply, State} | @@ -240,39 +230,85 @@ init([Request, Options, ProfileName]) -> %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling call messages %%-------------------------------------------------------------------- -handle_call(Request, _, State = #state{session = Session = - #tcp_session{socket = Socket, - type = pipeline}, - timers = Timers, - options = Options, - profile_name = ProfileName}) -> + + +%% This is the first request, the reason the proc was started +handle_call({connect_and_send, #request{address = Address0, + scheme = Scheme} = Request}, + _From, + #state{options = #options{proxy = Proxy}, + status = undefined, + session = undefined} = State) -> + ?hcrv("connect and send", [{address0, Address0}, {proxy, Proxy}]), + Address = handle_proxy(Address0, Proxy), + if + ((Address =/= Address0) andalso (Scheme =:= https)) -> + %% This is what we should do if and when ssl supports + %% "socket upgrading" + %%send_ssl_tunnel_request(Address, Request, + %% #state{options = Options, + %% status = ssl_tunnel}); + Reason = https_through_proxy_is_not_currently_supported, + Error = {error, Reason}, + {stop, Error, Error, State}; + true -> + case connect_and_send_first_request(Address, Request, State) of + {ok, NewState} -> + {reply, ok, NewState}; + {stop, Error, NewState} -> + {stop, Error, Error, NewState} + end + end; + +handle_call(Request, _, + #state{status = Status, + session = #tcp_session{socket = Socket, + type = pipeline} = Session, + timers = Timers, + options = Options, + profile_name = ProfileName} = State) + when Status =/= undefined -> + + ?hcrv("new request", [{request, Request}, + {profile, ProfileName}, + {status, Status}, + {session_type, pipeline}, + {timers, Timers}]), + Address = handle_proxy(Request#request.address, Options#options.proxy), case httpc_request:send(Address, Request, Socket) of ok -> + + ?hcrd("request sent", []), + %% Activate the request time out for the new request - NewState = activate_request_timeout(State#state{request = - Request}), + NewState = + activate_request_timeout(State#state{request = Request}), + + ClientClose = + httpc_request:is_client_closing(Request#request.headers), - ClientClose = httpc_request:is_client_closing( - Request#request.headers), case State#state.request of - #request{} -> %% Old request no yet finished + #request{} -> %% Old request not yet finished + ?hcrd("old request still not finished", []), %% Make sure to use the new value of timers in state - NewTimers = NewState#state.timers, + NewTimers = NewState#state.timers, NewPipeline = queue:in(Request, State#state.pipeline), - NewSession = + NewSession = Session#tcp_session{queue_length = %% Queue + current queue:len(NewPipeline) + 1, client_close = ClientClose}, httpc_manager:insert_session(NewSession, ProfileName), + ?hcrd("session updated", []), {reply, ok, State#state{pipeline = NewPipeline, - session = NewSession, - timers = NewTimers}}; + session = NewSession, + timers = NewTimers}}; undefined -> - %% Note: tcp-message reciving has already been + %% Note: tcp-message receiving has already been %% activated by handle_pipeline/2. + ?hcrd("no current request", []), cancel_timer(Timers#timers.queue_timer, timeout_queue), NewSession = @@ -281,54 +317,67 @@ handle_call(Request, _, State = #state{session = Session = httpc_manager:insert_session(NewSession, ProfileName), Relaxed = (Request#request.settings)#http_options.relaxed, - {reply, ok, - NewState#state{request = Request, - session = NewSession, - mfa = {httpc_response, parse, - [State#state.max_header_size, - Relaxed]}, - timers = - Timers#timers{queue_timer = - undefined}}} + MFA = {httpc_response, parse, + [State#state.max_header_size, Relaxed]}, + NewTimers = Timers#timers{queue_timer = undefined}, + ?hcrd("session created", []), + {reply, ok, NewState#state{request = Request, + session = NewSession, + mfa = MFA, + timers = NewTimers}} end; {error, Reason} -> + ?hcri("failed sending request", [{reason, Reason}]), {reply, {pipeline_failed, Reason}, State} end; -handle_call(Request, _, #state{session = Session = - #tcp_session{type = keep_alive, - socket = Socket}, - timers = Timers, - options = Options, - profile_name = ProfileName} = State) -> - - ClientClose = httpc_request:is_client_closing(Request#request.headers), +handle_call(Request, _, + #state{status = Status, + session = #tcp_session{socket = Socket, + type = keep_alive} = Session, + timers = Timers, + options = Options, + profile_name = ProfileName} = State) + when Status =/= undefined -> - Address = handle_proxy(Request#request.address, - Options#options.proxy), + ?hcrv("new request", [{request, Request}, + {profile, ProfileName}, + {status, Status}, + {session_type, keep_alive}]), + + Address = handle_proxy(Request#request.address, Options#options.proxy), case httpc_request:send(Address, Request, Socket) of ok -> + + ?hcrd("request sent", []), + + %% Activate the request time out for the new request NewState = - activate_request_timeout(State#state{request = - Request}), + activate_request_timeout(State#state{request = Request}), + + ClientClose = + httpc_request:is_client_closing(Request#request.headers), case State#state.request of #request{} -> %% Old request not yet finished %% Make sure to use the new value of timers in state - NewTimers = NewState#state.timers, + ?hcrd("old request still not finished", []), + NewTimers = NewState#state.timers, NewKeepAlive = queue:in(Request, State#state.keep_alive), - NewSession = + NewSession = Session#tcp_session{queue_length = %% Queue + current queue:len(NewKeepAlive) + 1, client_close = ClientClose}, httpc_manager:insert_session(NewSession, ProfileName), + ?hcrd("session updated", []), {reply, ok, State#state{keep_alive = NewKeepAlive, - session = NewSession, - timers = NewTimers}}; + session = NewSession, + timers = NewTimers}}; undefined -> %% Note: tcp-message reciving has already been %% activated by handle_pipeline/2. + ?hcrd("no current request", []), cancel_timer(Timers#timers.queue_timer, timeout_queue), NewSession = @@ -337,17 +386,19 @@ handle_call(Request, _, #state{session = Session = httpc_manager:insert_session(NewSession, ProfileName), Relaxed = (Request#request.settings)#http_options.relaxed, - {reply, ok, - NewState#state{request = Request, - session = NewSession, - mfa = {httpc_response, parse, - [State#state.max_header_size, - Relaxed]}}} + MFA = {httpc_response, parse, + [State#state.max_header_size, Relaxed]}, + {reply, ok, NewState#state{request = Request, + session = NewSession, + mfa = MFA}} end; - {error, Reason} -> + + {error, Reason} -> + ?hcri("failed sending request", [{reason, Reason}]), {reply, {request_failed, Reason}, State} end. + %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | @@ -367,16 +418,28 @@ handle_call(Request, _, #state{session = Session = %% handle_keep_alive_queue/2 on the other hand will just skip the %% request as if it was never issued as in this case the request will %% not have been sent. -handle_cast({cancel, RequestId}, State = #state{request = Request = - #request{id = RequestId}, - profile_name = ProfileName}) -> +handle_cast({cancel, RequestId}, + #state{request = #request{id = RequestId} = Request, + profile_name = ProfileName, + canceled = Canceled} = State) -> + ?hcrv("cancel current request", [{request_id, RequestId}, + {profile, ProfileName}, + {canceled, Canceled}]), httpc_manager:request_canceled(RequestId, ProfileName), + ?hcrv("canceled", []), {stop, normal, - State#state{canceled = [RequestId | State#state.canceled], - request = Request#request{from = answer_sent}}}; -handle_cast({cancel, RequestId}, State = #state{profile_name = ProfileName}) -> + State#state{canceled = [RequestId | Canceled], + request = Request#request{from = answer_sent}}}; +handle_cast({cancel, RequestId}, + #state{profile_name = ProfileName, + canceled = Canceled} = State) -> + ?hcrv("cancel", [{request_id, RequestId}, + {profile, ProfileName}, + {canceled, Canceled}]), httpc_manager:request_canceled(RequestId, ProfileName), - {noreply, State#state{canceled = [RequestId | State#state.canceled]}}; + ?hcrv("canceled", []), + {noreply, State#state{canceled = [RequestId | Canceled]}}; + handle_cast(stream_next, #state{session = Session} = State) -> http_transport:setopts(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), @@ -399,7 +462,13 @@ handle_info({Proto, _Socket, Data}, (Proto =:= ssl) orelse (Proto =:= httpc_handler) -> - ?hcri("received data", [{proto, Proto}, {data, Data}, {mfa, MFA}, {method, Method}, {stream, Stream}, {session, Session}, {status_line, StatusLine}]), + ?hcri("received data", [{proto, Proto}, + {data, Data}, + {mfa, MFA}, + {method, Method}, + {stream, Stream}, + {session, Session}, + {status_line, StatusLine}]), FinalResult = try Module:Function([Data | Args]) of @@ -410,22 +479,23 @@ handle_info({Proto, _Socket, Data}, ?hcrd("data processed - whole body", []), handle_response(State#state{body = <<>>}); {Module, whole_body, [Body, Length]} -> - ?hcrd("data processed - whole body", [{module, Module}, {body, Body}, {length, Length}]), + ?hcrd("data processed - whole body", + [{module, Module}, {body, Body}, {length, Length}]), {_, Code, _} = StatusLine, {NewBody, NewRequest} = stream(Body, Request, Code), %% When we stream we will not keep the already %% streamed data, that would be a waste of memory. - NewLength = case Stream of - none -> - Length; - _ -> - Length - size(Body) - end, + NewLength = + case Stream of + none -> + Length; + _ -> + Length - size(Body) + end, NewState = next_body_chunk(State), - - {noreply, NewState#state{mfa = {Module, whole_body, - [NewBody, NewLength]}, + NewMFA = {Module, whole_body, [NewBody, NewLength]}, + {noreply, NewState#state{mfa = NewMFA, request = NewRequest}}; NewMFA -> ?hcrd("data processed", [{new_mfa, NewMFA}]), @@ -435,16 +505,14 @@ handle_info({Proto, _Socket, Data}, {noreply, State#state{mfa = NewMFA}} catch exit:_ -> - ClientErrMsg = httpc_response:error(Request, - {could_not_parse_as_http, - Data}), - NewState = answer_request(Request, ClientErrMsg, State), + ClientReason = {could_not_parse_as_http, Data}, + ClientErrMsg = httpc_response:error(Request, ClientReason), + NewState = answer_request(Request, ClientErrMsg, State), {stop, normal, NewState}; - error:_ -> - ClientErrMsg = httpc_response:error(Request, - {could_not_parse_as_http, - Data}), - NewState = answer_request(Request, ClientErrMsg, State), + error:_ -> + ClientReason = {could_not_parse_as_http, Data}, + ClientErrMsg = httpc_response:error(Request, ClientReason), + NewState = answer_request(Request, ClientErrMsg, State), {stop, normal, NewState} end, @@ -453,10 +521,10 @@ handle_info({Proto, _Socket, Data}, handle_info({Proto, Socket, Data}, - #state{mfa = MFA, - request = Request, - session = Session, - status = Status, + #state{mfa = MFA, + request = Request, + session = Session, + status = Status, status_line = StatusLine, profile_name = Profile} = State) when (Proto =:= tcp) orelse @@ -474,6 +542,7 @@ handle_info({Proto, Socket, Data}, "~n", [Proto, Socket, Data, MFA, Request, Session, Status, StatusLine, Profile]), + {noreply, State}; @@ -513,28 +582,35 @@ handle_info({ssl_error, _, _} = Reason, State) -> handle_info({timeout, RequestId}, #state{request = #request{id = RequestId} = Request, canceled = Canceled} = State) -> + ?hcri("timeout of current request", [{id, RequestId}]), httpc_response:send(Request#request.from, - httpc_response:error(Request,timeout)), + httpc_response:error(Request, timeout)), + ?hcrv("response (timeout) sent - now terminate", []), {stop, normal, State#state{request = Request#request{from = answer_sent}, canceled = [RequestId | Canceled]}}; handle_info({timeout, RequestId}, #state{canceled = Canceled} = State) -> + ?hcri("timeout", [{id, RequestId}]), Filter = fun(#request{id = Id, from = From} = Request) when Id =:= RequestId -> + ?hcrv("found request", [{id, Id}, {from, From}]), %% Notify the owner Response = httpc_response:error(Request, timeout), httpc_response:send(From, Response), + ?hcrv("response (timeout) sent", []), [Request#request{from = answer_sent}]; (_) -> true end, case State#state.status of pipeline -> + ?hcrd("pipeline", []), Pipeline = queue:filter(Filter, State#state.pipeline), {noreply, State#state{canceled = [RequestId | Canceled], pipeline = Pipeline}}; keep_alive -> + ?hcrd("keep_alive", []), KeepAlive = queue:filter(Filter, State#state.keep_alive), {noreply, State#state{canceled = [RequestId | Canceled], keep_alive = KeepAlive}} @@ -577,9 +653,10 @@ terminate(normal, #state{session = undefined}) -> %% Init error sending, no session information has been setup but %% there is a socket that needs closing. -terminate(normal, #state{request = Request, - session = #tcp_session{id = undefined, - socket = Socket}}) -> +terminate(normal, + #state{request = Request, + session = #tcp_session{id = undefined, + socket = Socket}}) -> http_transport:close(socket_type(Request), Socket); %% Socket closed remotely @@ -605,23 +682,28 @@ terminate(normal, %% And, just in case, close our side (**really** overkill) http_transport:close(socket_type(Request), Socket); -terminate(_, State = #state{session = Session, - request = undefined, - profile_name = ProfileName, - timers = Timers, - pipeline = Pipeline, - keep_alive = KeepAlive}) -> - catch httpc_manager:delete_session(Session#tcp_session.id, - ProfileName), +terminate(_, #state{session = #tcp_session{id = Id, + socket = Socket, + scheme = Scheme}, + request = undefined, + profile_name = ProfileName, + timers = Timers, + pipeline = Pipeline, + keep_alive = KeepAlive} = State) -> + (catch httpc_manager:delete_session(Id, ProfileName)), maybe_retry_queue(Pipeline, State), maybe_retry_queue(KeepAlive, State), cancel_timer(Timers#timers.queue_timer, timeout_queue), - Socket = Session#tcp_session.socket, - http_transport:close(socket_type(Session#tcp_session.scheme), Socket); + http_transport:close(socket_type(Scheme), Socket); -terminate(Reason, State = #state{request = Request}) -> +terminate(Reason, #state{request = undefined}) -> + ?hcrt("terminate", [{reason, Reason}]), + ok; + +terminate(Reason, #state{request = Request} = State) -> + ?hcrd("terminate", [{reason, Reason}, {request, Request}]), NewState = maybe_send_answer(Request, httpc_response:error(Request, Reason), State), @@ -641,13 +723,16 @@ maybe_send_answer(Request, Answer, State) -> answer_request(Request, Answer, State). deliver_answers([]) -> + ?hcrd("deliver answer done", []), ok; -deliver_answers([#request{from = From} = Request | Requests]) +deliver_answers([#request{id = Id, from = From} = Request | Requests]) when is_pid(From) -> Response = httpc_response:error(Request, socket_closed_remotely), + ?hcrd("deliver answer", [{id, Id}, {from, From}, {response, Response}]), httpc_response:send(From, Response), deliver_answers(Requests); -deliver_answers([_|Requests]) -> +deliver_answers([Request|Requests]) -> + ?hcrd("skip deliver answer", [{request, Request}]), deliver_answers(Requests). @@ -728,77 +813,58 @@ connect(SocketType, ToAddress, #options{ipfamily = IpFamily, http_transport:connect(SocketType, ToAddress, Opts3, Timeout) end. - -send_first_request(Address, Request, #state{options = Options} = State) -> - SocketType = socket_type(Request), - ConnTimeout = (Request#request.settings)#http_options.connect_timeout, - ?hcri("connect", +connect_and_send_first_request(Address, + #request{settings = Settings, + headers = Headers, + address = OrigAddress, + scheme = Scheme} = Request, + #state{options = Options} = State) -> + + ?hcrd("connect", [{address, Address}, {request, Request}, {options, Options}]), + + SocketType = socket_type(Request), + ConnTimeout = Settings#http_options.connect_timeout, case connect(SocketType, Address, Options, ConnTimeout) of {ok, Socket} -> - ?hcri("connected - now send first request", [{socket, Socket}]), + ?hcrd("connected - now send first request", [{socket, Socket}]), case httpc_request:send(Address, Request, Socket) of ok -> - ?hcri("first request sent", []), + ?hcrd("first request sent", []), ClientClose = - httpc_request:is_client_closing( - Request#request.headers), + httpc_request:is_client_closing(Headers), SessionType = httpc_manager:session_type(Options), Session = - #tcp_session{id = {Request#request.address, self()}, - scheme = Request#request.scheme, - socket = Socket, + #tcp_session{id = {OrigAddress, self()}, + scheme = Scheme, + socket = Socket, client_close = ClientClose, - type = SessionType}, - TmpState = State#state{request = Request, - session = Session, - mfa = init_mfa(Request, State), - status_line = - init_status_line(Request), - headers = undefined, - body = undefined, - status = new}, - http_transport:setopts(SocketType, - Socket, [{active, once}]), + type = SessionType}, + TmpState = + State#state{request = Request, + session = Session, + mfa = init_mfa(Request, State), + status_line = init_status_line(Request), + headers = undefined, + body = undefined, + status = new}, + ?hcrt("activate socket", []), + activate_once(Session), NewState = activate_request_timeout(TmpState), {ok, NewState}; - {error, Reason} -> - %% Commented out in wait of ssl support to avoid - %% dialyzer warning - %%case State#state.status of - %% new -> % Called from init/1 - self() ! {init_error, error_sending, - httpc_response:error(Request, Reason)}, - {ok, State#state{request = Request, - session = - #tcp_session{socket = Socket}}} - %%ssl_tunnel -> % Not called from init/1 - %% NewState = - %% answer_request(Request, - %%httpc_response:error(Request, - %%Reason), - %% State), - %% {stop, normal, NewState} - %% end + {error, Reason} -> + ?hcrv("failed sending request", [{reason, Reason}]), + Error = {error, {send_failed, + httpc_response:error(Request, Reason)}}, + {stop, Error, State#state{request = Request}} end; - {error, Reason} -> - %% Commented out in wait of ssl support to avoid - %% dialyzer warning - %% case State#state.status of - %% new -> % Called from init/1 - self() ! {init_error, error_connecting, - httpc_response:error(Request, Reason)}, - {ok, State#state{request = Request}} - %% ssl_tunnel -> % Not called from init/1 - %% NewState = - %% answer_request(Request, - %% httpc_response:error(Request, - %% Reason), - %% State), - %% {stop, normal, NewState} - %%end + {error, Reason} -> + ?hcri("connect failed", [{reason, Reason}]), + Error = {error, {connect_failed, + httpc_response:error(Request, Reason)}}, + {stop, Error, State#state{request = Request}} end. handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, @@ -806,23 +872,23 @@ handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, ?hcrt("handle_http_msg", [{body, Body}]), case Headers#http_response_h.'content-type' of "multipart/byteranges" ++ _Param -> - exit(not_yet_implemented); + exit({not_yet_implemented, multypart_nyteranges}); _ -> - StatusLine = {Version, StatusCode, ReasonPharse}, + StatusLine = {Version, StatusCode, ReasonPharse}, {ok, NewRequest} = start_stream(StatusLine, Headers, Request), handle_http_body(Body, State#state{request = NewRequest, status_line = StatusLine, headers = Headers}) end; -handle_http_msg({ChunkedHeaders, Body}, - State = #state{headers = Headers}) -> - ?hcrt("handle_http_msg", [{chunked_headers, ChunkedHeaders}, {body, Body}]), +handle_http_msg({ChunkedHeaders, Body}, #state{headers = Headers} = State) -> + ?hcrt("handle_http_msg", + [{chunked_headers, ChunkedHeaders}, {body, Body}]), NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), handle_response(State#state{headers = NewHeaders, body = Body}); -handle_http_msg(Body, State = #state{status_line = {_,Code, _}}) -> +handle_http_msg(Body, #state{status_line = {_,Code, _}} = State) -> ?hcrt("handle_http_msg", [{body, Body}, {code, Code}]), - {NewBody, NewRequest}= stream(Body, State#state.request, Code), + {NewBody, NewRequest} = stream(Body, State#state.request, Code), handle_response(State#state{body = NewBody, request = NewRequest}). handle_http_body(<<>>, State = #state{status_line = {_,304, _}}) -> @@ -837,11 +903,15 @@ handle_http_body(<<>>, State = #state{request = #request{method = head}}) -> ?hcrt("handle_http_body - head", []), handle_response(State#state{body = <<>>}); -handle_http_body(Body, State = #state{headers = Headers, +handle_http_body(Body, State = #state{headers = Headers, max_body_size = MaxBodySize, - status_line = {_,Code, _}, - request = Request}) -> - ?hcrt("handle_http_body", [{body, Body}, {max_body_size, MaxBodySize}, {code, Code}]), + status_line = {_,Code, _}, + request = Request}) -> + ?hcrt("handle_http_body", + [{headers, Headers}, + {body, Body}, + {max_body_size, MaxBodySize}, + {code, Code}]), TransferEnc = Headers#http_response_h.'transfer-encoding', case case_insensitive_header(TransferEnc) of "chunked" -> @@ -850,12 +920,17 @@ handle_http_body(Body, State = #state{headers = Headers, State#state.max_header_size, {Code, Request}) of {Module, Function, Args} -> - ?hcrt("handle_http_body - new mfa", [{module, Module}, {function, Function}, {args, Args}]), + ?hcrt("handle_http_body - new mfa", + [{module, Module}, + {function, Function}, + {args, Args}]), NewState = next_body_chunk(State), {noreply, NewState#state{mfa = {Module, Function, Args}}}; {ok, {ChunkedHeaders, NewBody}} -> - ?hcrt("handle_http_body - nyew body", [{chunked_headers, ChunkedHeaders}, {new_body, NewBody}]), + ?hcrt("handle_http_body - new body", + [{chunked_headers, ChunkedHeaders}, + {new_body, NewBody}]), NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), handle_response(State#state{headers = NewHeaders, @@ -872,12 +947,13 @@ handle_http_body(Body, State = #state{headers = Headers, ?hcrt("handle_http_body - other", []), Length = list_to_integer(Headers#http_response_h.'content-length'), - case ((Length =< MaxBodySize) or (MaxBodySize == nolimit)) of + case ((Length =< MaxBodySize) orelse (MaxBodySize =:= nolimit)) of true -> case httpc_response:whole_body(Body, Length) of {ok, Body} -> - {NewBody, NewRequest}= stream(Body, Request, Code), - handle_response(State#state{body = NewBody, + {NewBody, NewRequest} = + stream(Body, Request, Code), + handle_response(State#state{body = NewBody, request = NewRequest}); MFA -> NewState = next_body_chunk(State), @@ -893,42 +969,31 @@ handle_http_body(Body, State = #state{headers = Headers, end end. -%%% Normaly I do not comment out code, I throw it away. But this might -%%% actually be used on day if ssl is improved. -%% handle_response(State = #state{status = ssl_tunnel, -%% request = Request, -%% options = Options, -%% session = #tcp_session{socket = Socket, -%% scheme = Scheme}, -%% status_line = {_, 200, _}}) -> -%% %%% Insert code for upgrading the socket if and when ssl supports this. -%% Address = handle_proxy(Request#request.address, Options#options.proxy), -%% send_first_request(Address, Request, State); -%% handle_response(State = #state{status = ssl_tunnel, -%% request = Request}) -> -%% NewState = answer_request(Request, -%% httpc_response:error(Request, -%% ssl_proxy_tunnel_failed), -%% State), -%% {stop, normal, NewState}; - -handle_response(State = #state{status = new}) -> - handle_response(try_to_enable_pipeline_or_keep_alive(State)); - -handle_response(State = - #state{request = Request, +handle_response(#state{status = new} = State) -> + ?hcrd("handle response - status = new", []), + handle_response(try_to_enable_pipeline_or_keep_alive(State)); + +handle_response(#state{request = Request, status = Status, session = Session, status_line = StatusLine, headers = Headers, body = Body, options = Options, - profile_name = ProfileName}) when Status =/= new -> - ?hcrt("handle response", [{status, Status}, {session, Session}, {status_line, StatusLine}, {profile_name, ProfileName}]), + profile_name = ProfileName} = State) + when Status =/= new -> + + ?hcrd("handle response", [{profile, ProfileName}, + {status, Status}, + {request, Request}, + {session, Session}, + {status_line, StatusLine}]), + handle_cookies(Headers, Request, Options, ProfileName), case httpc_response:result({StatusLine, Headers, Body}, Request) of %% 100-continue continue -> + ?hcrd("handle response - continue", []), %% Send request body {_, RequestBody} = Request#request.content, http_transport:send(socket_type(Session#tcp_session.scheme), @@ -939,44 +1004,46 @@ handle_response(State = Session#tcp_session.socket, [{active, once}]), Relaxed = (Request#request.settings)#http_options.relaxed, - {noreply, - State#state{mfa = {httpc_response, parse, - [State#state.max_header_size, - Relaxed]}, - status_line = undefined, - headers = undefined, - body = undefined - }}; + MFA = {httpc_response, parse, + [State#state.max_header_size, Relaxed]}, + {noreply, State#state{mfa = MFA, + status_line = undefined, + headers = undefined, + body = undefined}}; + %% Ignore unexpected 100-continue response and receive the %% actual response that the server will send right away. {ignore, Data} -> + ?hcrd("handle response - ignore", [{data, Data}]), Relaxed = (Request#request.settings)#http_options.relaxed, - NewState = State#state{mfa = - {httpc_response, parse, - [State#state.max_header_size, - Relaxed]}, + MFA = {httpc_response, parse, + [State#state.max_header_size, Relaxed]}, + NewState = State#state{mfa = MFA, status_line = undefined, - headers = undefined, - body = undefined}, + headers = undefined, + body = undefined}, handle_info({httpc_handler, dummy, Data}, NewState); + %% On a redirect or retry the current request becomes %% obsolete and the manager will create a new request %% with the same id as the current. {redirect, NewRequest, Data} -> - ?hcrt("handle response - redirect", [{new_request, NewRequest}, {data, Data}]), + ?hcrt("handle response - redirect", + [{new_request, NewRequest}, {data, Data}]), ok = httpc_manager:redirect_request(NewRequest, ProfileName), handle_queue(State#state{request = undefined}, Data); {retry, TimeNewRequest, Data} -> - ?hcrt("handle response - retry", [{time_new_request, TimeNewRequest}, {data, Data}]), + ?hcrt("handle response - retry", + [{time_new_request, TimeNewRequest}, {data, Data}]), ok = httpc_manager:retry_request(TimeNewRequest, ProfileName), handle_queue(State#state{request = undefined}, Data); {ok, Msg, Data} -> - ?hcrt("handle response - result ok", [{msg, Msg}, {data, Data}]), + ?hcrd("handle response - ok", [{msg, Msg}, {data, Data}]), end_stream(StatusLine, Request), NewState = answer_request(Request, Msg, State), handle_queue(NewState, Data); {stop, Msg} -> - ?hcrt("handle response - result stop", [{msg, Msg}]), + ?hcrd("handle response - stop", [{msg, Msg}]), end_stream(StatusLine, Request), NewState = answer_request(Request, Msg, State), {stop, normal, NewState} @@ -990,60 +1057,67 @@ handle_cookies(_,_, #options{cookies = verify}, _) -> ok; handle_cookies(Headers, Request, #options{cookies = enabled}, ProfileName) -> {Host, _ } = Request#request.address, - Cookies = http_cookie:cookies(Headers#http_response_h.other, + Cookies = httpc_cookie:cookies(Headers#http_response_h.other, Request#request.path, Host), httpc_manager:store_cookies(Cookies, Request#request.address, ProfileName). %% This request could not be pipelined or used as sequential keept alive %% queue -handle_queue(State = #state{status = close}, _) -> +handle_queue(#state{status = close} = State, _) -> {stop, normal, State}; -handle_queue(State = #state{status = keep_alive}, Data) -> +handle_queue(#state{status = keep_alive} = State, Data) -> handle_keep_alive_queue(State, Data); -handle_queue(State = #state{status = pipeline}, Data) -> +handle_queue(#state{status = pipeline} = State, Data) -> handle_pipeline(State, Data). -handle_pipeline(State = - #state{status = pipeline, session = Session, +handle_pipeline(#state{status = pipeline, + session = Session, profile_name = ProfileName, - options = #options{pipeline_timeout = TimeOut}}, - Data) -> + options = #options{pipeline_timeout = TimeOut}} = + State, + Data) -> + + ?hcrd("handle pipeline", [{profile, ProfileName}, + {session, Session}, + {timeout, TimeOut}]), + case queue:out(State#state.pipeline) of {empty, _} -> + ?hcrd("epmty pipeline queue", []), + %% The server may choose too teminate an idle pipeline %% in this case we want to receive the close message %% at once and not when trying to pipeline the next %% request. - http_transport:setopts(socket_type(Session#tcp_session.scheme), - Session#tcp_session.socket, - [{active, once}]), + activate_once(Session), + %% If a pipeline that has been idle for some time is not %% closed by the server, the client may want to close it. - NewState = activate_queue_timeout(TimeOut, State), + NewState = activate_queue_timeout(TimeOut, State), NewSession = Session#tcp_session{queue_length = 0}, httpc_manager:insert_session(NewSession, ProfileName), %% Note mfa will be initilized when a new request %% arrives. {noreply, - NewState#state{request = undefined, - mfa = undefined, + NewState#state{request = undefined, + mfa = undefined, status_line = undefined, - headers = undefined, - body = undefined - } - }; + headers = undefined, + body = undefined}}; {{value, NextRequest}, Pipeline} -> case lists:member(NextRequest#request.id, State#state.canceled) of true -> + ?hcrv("next request had been cancelled", []), %% See comment for handle_cast({cancel, RequestId}) {stop, normal, State#state{request = NextRequest#request{from = answer_sent}}}; false -> + ?hcrv("next request", [{request, NextRequest}]), NewSession = Session#tcp_session{queue_length = %% Queue + current @@ -1051,15 +1125,16 @@ handle_pipeline(State = httpc_manager:insert_session(NewSession, ProfileName), Relaxed = (NextRequest#request.settings)#http_options.relaxed, + MFA = {httpc_response, + parse, + [State#state.max_header_size, Relaxed]}, NewState = - State#state{pipeline = Pipeline, - request = NextRequest, - mfa = {httpc_response, parse, - [State#state.max_header_size, - Relaxed]}, + State#state{pipeline = Pipeline, + request = NextRequest, + mfa = MFA, status_line = undefined, - headers = undefined, - body = undefined}, + headers = undefined, + body = undefined}, case Data of <<>> -> http_transport:setopts( @@ -1076,15 +1151,20 @@ handle_pipeline(State = end end. -handle_keep_alive_queue(State = #state{status = keep_alive, - session = Session, - profile_name = ProfileName, - options = #options{keep_alive_timeout - = TimeOut} - }, - Data) -> +handle_keep_alive_queue( + #state{status = keep_alive, + session = Session, + profile_name = ProfileName, + options = #options{keep_alive_timeout = TimeOut}} = State, + Data) -> + + ?hcrd("handle keep_alive", [{profile, ProfileName}, + {session, Session}, + {timeout, TimeOut}]), + case queue:out(State#state.keep_alive) of {empty, _} -> + ?hcrd("epmty keep_alive queue", []), %% The server may choose too terminate an idle keep_alive session %% in this case we want to receive the close message %% at once and not when trying to send the next @@ -1111,25 +1191,25 @@ handle_keep_alive_queue(State = #state{status = keep_alive, case lists:member(NextRequest#request.id, State#state.canceled) of true -> - handle_keep_alive_queue(State#state{keep_alive = - KeepAlive}, Data); + ?hcrv("next request has already been canceled", []), + handle_keep_alive_queue( + State#state{keep_alive = KeepAlive}, Data); false -> + ?hcrv("next request", [{request, NextRequest}]), Relaxed = (NextRequest#request.settings)#http_options.relaxed, + MFA = {httpc_response, parse, + [State#state.max_header_size, Relaxed]}, NewState = - State#state{request = NextRequest, - keep_alive = KeepAlive, - mfa = {httpc_response, parse, - [State#state.max_header_size, - Relaxed]}, + State#state{request = NextRequest, + keep_alive = KeepAlive, + mfa = MFA, status_line = undefined, - headers = undefined, - body = undefined}, + headers = undefined, + body = undefined}, case Data of <<>> -> - http_transport:setopts( - socket_type(Session#tcp_session.scheme), - Session#tcp_session.socket, [{active, once}]), + activate_once(Session), {noreply, NewState}; _ -> %% If we already received some bytes of @@ -1140,11 +1220,6 @@ handle_keep_alive_queue(State = #state{status = keep_alive, end end. -call(Msg, Pid, Timeout) -> - gen_server:call(Pid, Msg, Timeout). - -cast(Msg, Pid) -> - gen_server:cast(Pid, Msg). case_insensitive_header(Str) when is_list(Str) -> http_util:to_lower(Str); @@ -1152,20 +1227,34 @@ case_insensitive_header(Str) when is_list(Str) -> case_insensitive_header(Str) -> Str. -activate_request_timeout(State = #state{request = Request}) -> - Time = (Request#request.settings)#http_options.timeout, - case Time of +activate_once(#tcp_session{scheme = Scheme, socket = Socket}) -> + SocketType = socket_type(Scheme), + http_transport:setopts(SocketType, Socket, [{active, once}]). + +activate_request_timeout( + #state{request = #request{timer = undefined} = Request} = State) -> + Timeout = (Request#request.settings)#http_options.timeout, + case Timeout of infinity -> State; _ -> - Ref = erlang:send_after(Time, self(), - {timeout, Request#request.id}), - State#state - {timers = - #timers{request_timers = - [{Request#request.id, Ref}| - (State#state.timers)#timers.request_timers]}} - end. + ReqId = Request#request.id, + ?hcrt("activate request timer", + [{request_id, ReqId}, + {time_consumed, t() - Request#request.started}, + {timeout, Timeout}]), + Msg = {timeout, ReqId}, + Ref = erlang:send_after(Timeout, self(), Msg), + Request2 = Request#request{timer = Ref}, + ReqTimers = [{Request#request.id, Ref} | + (State#state.timers)#timers.request_timers], + Timers = #timers{request_timers = ReqTimers}, + State#state{request = Request2, timers = Timers} + end; + +%% Timer is already running! This is the case for a redirect or retry +activate_request_timeout(State) -> + State. activate_queue_timeout(infinity, State) -> State; @@ -1191,12 +1280,12 @@ is_keep_alive_connection(Headers, Session) -> (not ((Session#tcp_session.client_close) or httpc_response:is_server_closing(Headers))). -try_to_enable_pipeline_or_keep_alive(State = - #state{session = Session, - request = #request{method = Method}, - status_line = {Version, _, _}, - headers = Headers, - profile_name = ProfileName}) -> +try_to_enable_pipeline_or_keep_alive( + #state{session = Session, + request = #request{method = Method}, + status_line = {Version, _, _}, + headers = Headers, + profile_name = ProfileName} = State) -> case (is_keep_alive_enabled_server(Version, Headers) andalso is_keep_alive_connection(Headers, Session)) of true -> @@ -1209,15 +1298,16 @@ try_to_enable_pipeline_or_keep_alive(State = httpc_manager:insert_session(Session, ProfileName), %% Make sure type is keep_alive in session %% as it in this case might be pipeline - State#state{status = keep_alive, - session = - Session#tcp_session{type = keep_alive}} + NewSession = Session#tcp_session{type = keep_alive}, + State#state{status = keep_alive, + session = NewSession} end; false -> State#state{status = close} end. -answer_request(Request, Msg, #state{timers = Timers} = State) -> +answer_request(Request, Msg, #state{timers = Timers} = State) -> + ?hcrt("answer request", [{request, Request}, {msg, Msg}]), httpc_response:send(Request#request.from, Msg), RequestTimers = Timers#timers.request_timers, TimerRef = @@ -1253,14 +1343,14 @@ retry_pipeline([Request | PipeLine], case (catch httpc_manager:retry_request(Request, ProfileName)) of ok -> RequestTimers = Timers#timers.request_timers, + ReqId = Request#request.id, TimerRef = - proplists:get_value(Request#request.id, RequestTimers, - undefined), - cancel_timer(TimerRef, {timeout, Request#request.id}), - State#state{timers = Timers#timers{request_timers = - lists:delete({Request#request.id, - TimerRef}, - RequestTimers)}}; + proplists:get_value(ReqId, RequestTimers, undefined), + cancel_timer(TimerRef, {timeout, ReqId}), + NewReqsTimers = lists:delete({ReqId, TimerRef}, RequestTimers), + NewTimers = Timers#timers{request_timers = NewReqsTimers}, + State#state{timers = NewTimers}; + Error -> answer_request(Request#request.from, httpc_response:error(Request, Error), State) @@ -1347,10 +1437,12 @@ socket_type(http) -> socket_type(https) -> {ssl, []}. %% Dummy value ok for ex setops that does not use this value -start_stream({_Version, _Code, _ReasonPhrase}, _Headers, #request{stream = none} = Request) -> +start_stream({_Version, _Code, _ReasonPhrase}, _Headers, + #request{stream = none} = Request) -> ?hcrt("start stream - none", []), {ok, Request}; -start_stream({_Version, Code, _ReasonPhrase}, Headers, #request{stream = self} = Request) +start_stream({_Version, Code, _ReasonPhrase}, Headers, + #request{stream = self} = Request) when (Code =:= 200) orelse (Code =:= 206) -> ?hcrt("start stream - self", [{code, Code}]), Msg = httpc_response:stream_start(Headers, Request, ignore), @@ -1363,7 +1455,8 @@ start_stream({_Version, Code, _ReasonPhrase}, Headers, Msg = httpc_response:stream_start(Headers, Request, self()), httpc_response:send(Request#request.from, Msg), {ok, Request}; -start_stream({_Version, Code, _ReasonPhrase}, _Headers, #request{stream = Filename} = Request) +start_stream({_Version, Code, _ReasonPhrase}, _Headers, + #request{stream = Filename} = Request) when ((Code =:= 200) orelse (Code =:= 206)) andalso is_list(Filename) -> ?hcrt("start stream", [{code, Code}, {filename, Filename}]), case file:open(Filename, [write, raw, append, delayed_write]) of @@ -1497,3 +1590,21 @@ handle_verbose(_) -> %% d(_, _, _) -> %% ok. + +call(Msg, Pid) -> + Timeout = infinity, + call(Msg, Pid, Timeout). +call(Msg, Pid, Timeout) -> + gen_server:call(Pid, Msg, Timeout). + +cast(Msg, Pid) -> + gen_server:cast(Pid, Msg). + + +%% to(To, Start) when is_integer(Start) andalso (Start >= 0) -> +%% http_util:timeout(To, Start); +%% to(To, _Start) -> +%% http_util:timeout(To, t()). + +t() -> + http_util:timestamp(). diff --git a/lib/inets/src/http_client/httpc_handler_sup.erl b/lib/inets/src/http_client/httpc_handler_sup.erl index d9edaa0599..2a69fd15d0 100644 --- a/lib/inets/src/http_client/httpc_handler_sup.erl +++ b/lib/inets/src/http_client/httpc_handler_sup.erl @@ -1,19 +1,19 @@ %% %% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2007-2009. All Rights Reserved. -%% +%% +%% Copyright Ericsson AB 2007-2010. All Rights Reserved. +%% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. -%% +%% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. -%% +%% %% %CopyrightEnd% %% %% @@ -23,7 +23,7 @@ %% API -export([start_link/0]). --export([start_child/1]). +-export([start_child/2]). %% Supervisor callback -export([init/1]). @@ -34,25 +34,28 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -start_child(Args) -> +start_child(Options, Profile) -> + Args = [Options, Profile], supervisor:start_child(?MODULE, Args). - + + %%%========================================================================= %%% Supervisor callback %%%========================================================================= init(Args) -> + RestartStrategy = simple_one_for_one, MaxR = 0, MaxT = 3600, - Name = undefined, % As simple_one_for_one is used. + Name = undefined, % As simple_one_for_one is used. StartFunc = {httpc_handler, start_link, Args}, - Restart = temporary, % E.g. should not be restarted - Shutdown = 4000, - Modules = [httpc_handler], - Type = worker, - + Restart = temporary, % E.g. should not be restarted + Shutdown = 4000, + Modules = [httpc_handler], + Type = worker, ChildSpec = {Name, StartFunc, Restart, Shutdown, Type, Modules}, + {ok, {{RestartStrategy, MaxR, MaxT}, [ChildSpec]}}. -- cgit v1.2.3