diff options
Diffstat (limited to 'src/couch_inets/http.erl')
-rw-r--r-- | src/couch_inets/http.erl | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/src/couch_inets/http.erl b/src/couch_inets/http.erl new file mode 100644 index 00000000..bdc7f73a --- /dev/null +++ b/src/couch_inets/http.erl @@ -0,0 +1,396 @@ +% ``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 via the world wide web 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. +%% +%% The Initial Developer of the Original Code is Ericsson Utvecklings AB. +%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings +%% AB. All Rights Reserved.'' +%% +%% $Id$ +%% +%% This module is very loosely based on code initially developed by +%% Johan Blom at Mobile Arts AB +%% Description: +%%% This version of the HTTP/1.1 client supports: +%%% - RFC 2616 HTTP 1.1 client part +%%% - RFC 2818 HTTP Over TLS + +-module(http). + +%% API +-export([request/1, request/4, cancel_request/1, set_options/1, + verify_cookies/2, cookie_header/1]). + +-include("http_internal.hrl"). +-include("httpc_internal.hrl"). + +%%%========================================================================= +%%% API +%%%========================================================================= + +%%-------------------------------------------------------------------------- +%% request(Method, Request, HTTPOptions, Options) -> +%% {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} | {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}</v> +%% 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(Url) -> + request(get, {Url, []}, [], []). + +request(Method, {Url, Headers}, HTTPOptions, Options) + when Method==options;Method==get;Method==head;Method==delete;Method==trace -> + case http_uri:parse(Url) of + {error,Reason} -> + {error,Reason}; + ParsedUrl -> + handle_request(Method, Url, {ParsedUrl, Headers, [], []}, + HTTPOptions, Options) + end; + +request(Method, {Url,Headers,ContentType,Body}, HTTPOptions, Options) + when Method==post;Method==put -> + case http_uri:parse(Url) of + {error,Reason} -> + {error,Reason}; + ParsedUrl -> + handle_request(Method, Url, + {ParsedUrl, Headers, ContentType, Body}, + HTTPOptions, Options) + end. + +%%-------------------------------------------------------------------------- +%% request(RequestId) -> ok +%% RequestId - As returned by request/4 +%% +%% Description: Cancels a HTTP-request. +%%------------------------------------------------------------------------- +cancel_request(RequestId) -> + ok = httpc_manager:cancel_request(RequestId), + receive + %% If the request was allready fullfilled throw away the + %% answer as the request has been canceled. + {http, {RequestId, _}} -> + ok + after 0 -> + ok + end. + +%%-------------------------------------------------------------------------- +%% set_options(Options) -> +%% Options - [Option] +%% Option - {proxy, {Proxy, NoProxy}} | {max_sessions, MaxSessions} | +%% {max_pipeline_length, MaxPipeline} | +%% {pipeline_timeout, PipelineTimeout} | {cookies, CookieMode} +%% | {ipv6, Ipv6Mode} +%% Proxy - {Host, Port} +%% NoProxy - [Domain | HostName | IPAddress] +%% MaxSessions, MaxPipeline, PipelineTimeout = integer() +%% CookieMode - enabled | disabled | verify +%% Ipv6Mode - enabled | disabled +%% Description: Informs the httpc_manager of the new settings. +%%------------------------------------------------------------------------- +set_options(Options) -> + ensure_started(no_scheme), + httpc_manager:set_options(Options). + +verify_cookies(SetCookieHeaders, Url) -> + {_, _, Host, Port, Path, _} = http_uri:parse(Url), + Cookies = http_cookie:cookies(SetCookieHeaders, Path, Host), + httpc_manager:store_cookies(Cookies, {Host, Port}), + ok. + +cookie_header(Url) -> + httpc_manager:cookies(Url). + +%%%======================================================================== +%%% Internal functions +%%%======================================================================== +handle_request(Method, Url, {{Scheme, UserInfo, Host, Port, Path, Query}, + Headers, ContentType, Body}, HTTPOptions, Options) -> + HTTPRecordOptions = http_options(HTTPOptions, #http_options{}), + + Sync = http_util:key1search(Options, sync, true), + NewHeaders = lists:map(fun({Key, Val}) -> + {http_util:to_lower(Key), Val} end, + Headers), + Stream = http_util:key1search(Options, stream, none), + + case {Sync, Stream} of + {true, self} -> + {error, streaming_error}; + _ -> + RecordHeaders = header_record(NewHeaders, #http_request_h{}, Host), + Request = #request{from = self(), + scheme = Scheme, address = {Host,Port}, + path = Path, pquery = Query, method = Method, + headers = RecordHeaders, + content = {ContentType,Body}, + settings = HTTPRecordOptions, + abs_uri = Url, userinfo = UserInfo, + stream = Stream, + headers_as_is = + headers_as_is(Headers, Options)}, + + ensure_started(Scheme), + + case httpc_manager:request(Request) of + {ok, RequestId} -> + handle_answer(RequestId, Sync, Options); + {error, Reason} -> + {error, Reason} + end + 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, {StatusLine, Headers, BinBody}) -> + Body = + case http_util:key1search(Options, body_format, string) of + string -> + binary_to_list(BinBody); + _ -> + BinBody + end, + case http_util:key1search(Options, full_result, true) of + true -> + {ok, {StatusLine, Headers, Body}}; + false -> + {_, Status, _} = StatusLine, + {ok, {Status, Body}} + 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 http_util:key1search(Options, headers_as_is, false) of + false -> + []; + true -> + Headers + end. + +http_options([], Acc) -> + Acc; +http_options([{timeout, Val} | Settings], Acc) + when is_integer(Val), Val >= 0-> + http_options(Settings, Acc#http_options{timeout = Val}); +http_options([{timeout, infinity} | Settings], Acc) -> + http_options(Settings, Acc#http_options{timeout = infinity}); +http_options([{autoredirect, Val} | Settings], Acc) + when Val == true; Val == false -> + http_options(Settings, Acc#http_options{autoredirect = Val}); +http_options([{ssl, Val} | Settings], Acc) -> + http_options(Settings, Acc#http_options{ssl = Val}); +http_options([{relaxed, Val} | Settings], Acc) + when Val == true; Val == false -> + http_options(Settings, Acc#http_options{relaxed = Val}); +http_options([{proxy_auth, Val = {User, Passwd}} | Settings], Acc) + when is_list(User), + is_list(Passwd) -> + http_options(Settings, Acc#http_options{proxy_auth = Val}); +http_options([Option | Settings], Acc) -> + error_logger:info_report("Invalid option ignored ~p~n", [Option]), + http_options(Settings, Acc). + +header_record([], RequestHeaders, Host) -> + validate_headers(RequestHeaders, Host); +header_record([{"cache-control", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'cache-control' = Val}, + Host); +header_record([{"connection", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{connection = Val}, Host); +header_record([{"date", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{date = Val}, Host); +header_record([{"pragma", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{pragma = Val}, Host); +header_record([{"trailer", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{trailer = Val}, Host); +header_record([{"transfer-encoding", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, + RequestHeaders#http_request_h{'transfer-encoding' = Val}, + Host); +header_record([{"upgrade", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{upgrade = Val}, Host); +header_record([{"via", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{via = Val}, Host); +header_record([{"warning", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{warning = Val}, Host); +header_record([{"accept", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{accept = Val}, Host); +header_record([{"accept-charset", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'accept-charset' = Val}, + Host); +header_record([{"accept-encoding", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'accept-encoding' = Val}, + Host); +header_record([{"accept-language", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'accept-language' = Val}, + Host); +header_record([{"authorization", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{authorization = Val}, + Host); +header_record([{"expect", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{expect = Val}, Host); +header_record([{"from", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{from = Val}, Host); +header_record([{"host", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{host = Val}, Host); +header_record([{"if-match", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'if-match' = Val}, + Host); +header_record([{"if-modified-since", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, + RequestHeaders#http_request_h{'if-modified-since' = Val}, + Host); +header_record([{"if-none-match", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'if-none-match' = Val}, + Host); +header_record([{"if-range", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'if-range' = Val}, + Host); + +header_record([{"if-unmodified-since", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'if-unmodified-since' + = Val}, Host); +header_record([{"max-forwards", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'max-forwards' = Val}, + Host); +header_record([{"proxy-authorization", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'proxy-authorization' + = Val}, Host); +header_record([{"range", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{range = Val}, Host); +header_record([{"referer", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{referer = Val}, Host); +header_record([{"te", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{te = Val}, Host); +header_record([{"user-agent", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'user-agent' = Val}, + Host); +header_record([{"allow", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{allow = Val}, Host); +header_record([{"content-encoding", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, + RequestHeaders#http_request_h{'content-encoding' = Val}, + Host); +header_record([{"content-language", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, + RequestHeaders#http_request_h{'content-language' = Val}, + Host); +header_record([{"content-length", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'content-length' = Val}, + Host); +header_record([{"content-location", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, + RequestHeaders#http_request_h{'content-location' = Val}, + Host); +header_record([{"content-md5", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'content-md5' = Val}, + Host); +header_record([{"content-range", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'content-range' = Val}, + Host); +header_record([{"content-type", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'content-type' = Val}, + Host); +header_record([{"expires", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{expires = Val}, Host); +header_record([{"last-modified", Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{'last-modified' = Val}, + Host); +header_record([{Key, Val} | Rest], RequestHeaders, Host) -> + header_record(Rest, RequestHeaders#http_request_h{ + other = [{Key, Val} | + RequestHeaders#http_request_h.other]}, + Host). + +validate_headers(RequestHeaders = #http_request_h{te = undefined}, Host) -> + validate_headers(RequestHeaders#http_request_h{te = ""}, Host); +validate_headers(RequestHeaders = #http_request_h{host = undefined}, Host) -> + validate_headers(RequestHeaders#http_request_h{host = Host}, Host); +validate_headers(RequestHeaders, _) -> + RequestHeaders. + +ensure_started(Scheme) -> + %% Start of the inets application should really be handled by the + %% application using inets. + case application:start(couch_inets) of + {error,{already_started,couch_inets}} -> + ok; + {error, {{already_started,_}, % Started as an included application + {inets_app, start, _}}} -> + ok; + ok -> + error_logger:info_report("The inets application was not started." + " Has now been started as a temporary" + " application.") + end, + + case Scheme of + https -> + %% Start of the ssl application should really be handled by the + %% application using inets. + case application:start(ssl) of + {error,{already_started,ssl}} -> + ok; + %% Started as an included application + {error, {{already_started,_}, + {ssl_app, start, _}}} -> + ok; + ok -> + error_logger:info_report("The ssl application was not " + "started. Has now been started " + "as a temporary application.") + end; + _ -> + ok + end. |