aboutsummaryrefslogblamecommitdiffstats
path: root/src/cowboy_http_req.erl
blob: 8ea8efc9c144655423365d17dbf70f6cb3a34dd7 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                                                         
                                                         













                                                                           
 
         
                                    





                                                


                   
                                 


                        

                    




                                        
               










                                                                               


                                                                          


















                                                                          
                                              
                                                                  
                                                           
                    
                                     
 
                                                       

                                                               
                                                                        
                                 

                                                            



                                                            
 
                               
                                                                              










                                                                      
                                                           
                                                             
                     
                                      
 
                                                      
                                                                              
                              

                                                             
                                       
            
 





                                                                     
                                                           
                                                           
                    
                                     
 
                                                                
                                                                              
                             

                                                            
                                       
            
 




                                                                                

                    
                                                
                            
                                                                             


                                                                       
                                             





                                                          
                                                       
                                                                             

                                                              





                                                                        
                               
                                                                              



                                               


                                                         
                                                                    

                                                       

                                                           




                                                                                    
                                            
 

            
                                                                               









                                                                       





























































                                                              
















                                                                     
%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

-module(cowboy_http_req).

-export([
	method/1, version/1, peer/1,
	host/1, raw_host/1,
	path/1, raw_path/1,
	qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
	binding/2, binding/3, bindings/1,
	header/2, header/3, headers/1
%%	cookie/2, cookie/3, cookies/1 @todo
]). %% Request API.

-export([
	body/1, body/2, body_qs/1
]). %% Request Body API.

-export([
	reply/4
]). %% Response API.

-include("include/types.hrl").
-include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl").

%% Request API.

-spec method(Req::#http_req{}) -> {Method::http_method(), Req::#http_req{}}.
method(Req) ->
	{Req#http_req.method, Req}.

-spec version(Req::#http_req{}) -> {Version::http_version(), Req::#http_req{}}.
version(Req) ->
	{Req#http_req.version, Req}.

-spec peer(Req::#http_req{})
	-> {{Address::ip_address(), Port::port_number()}, Req::#http_req{}}.
peer(Req=#http_req{socket=Socket, transport=Transport, peer=undefined}) ->
	{ok, Peer} = Transport:peername(Socket),
	{Peer, Req#http_req{peer=Peer}};
peer(Req) ->
	{Req#http_req.peer, Req}.

-spec host(Req::#http_req{}) -> {Host::path_tokens(), Req::#http_req{}}.
host(Req) ->
	{Req#http_req.host, Req}.

-spec raw_host(Req::#http_req{}) -> {RawHost::string(), Req::#http_req{}}.
raw_host(Req) ->
	{Req#http_req.raw_host, Req}.

-spec path(Req::#http_req{}) -> {Path::path_tokens(), Req::#http_req{}}.
path(Req) ->
	{Req#http_req.path, Req}.

-spec raw_path(Req::#http_req{}) -> {RawPath::string(), Req::#http_req{}}.
raw_path(Req) ->
	{Req#http_req.raw_path, Req}.

-spec qs_val(Name::string(), Req::#http_req{})
	-> {Value::string() | true | undefined, Req::#http_req{}}.
%% @equiv qs_val(Name, Req) -> qs_val(Name, Req, undefined)
qs_val(Name, Req) ->
	qs_val(Name, Req, undefined).

-spec qs_val(Name::string(), Req::#http_req{}, Default)
	-> {Value::string() | true | Default, Req::#http_req{}}
	when Default::term().
qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default) ->
	QsVals = parse_qs(RawQs),
	qs_val(Name, Req#http_req{qs_vals=QsVals}, Default);
qs_val(Name, Req, Default) ->
	case lists:keyfind(Name, 1, Req#http_req.qs_vals) of
		{Name, Value} -> {Value, Req};
		false -> {Default, Req}
	end.

-spec qs_vals(Req::#http_req{})
	-> {list({Name::string(), Value::string() | true}), Req::#http_req{}}.
qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
	QsVals = parse_qs(RawQs),
	qs_vals(Req#http_req{qs_vals=QsVals});
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
	{QsVals, Req}.

-spec raw_qs(Req::#http_req{}) -> {RawQs::string(), Req::#http_req{}}.
raw_qs(Req) ->
	{Req#http_req.raw_qs, Req}.

-spec binding(Name::atom(), Req::#http_req{})
	-> {Value::string() | undefined, Req::#http_req{}}.
%% @equiv binding(Name, Req) -> binding(Name, Req, undefined)
binding(Name, Req) ->
	binding(Name, Req, undefined).

-spec binding(Name::atom(), Req::#http_req{}, Default)
	-> {Value::string() | Default, Req::#http_req{}} when Default::term().
binding(Name, Req, Default) ->
	case lists:keyfind(Name, 1, Req#http_req.bindings) of
		{Name, Value} -> {Value, Req};
		false -> {Default, Req}
	end.

-spec bindings(Req::#http_req{})
	-> {list({Name::atom(), Value::string()}), Req::#http_req{}}.
bindings(Req) ->
	{Req#http_req.bindings, Req}.

-spec header(Name::atom() | string(), Req::#http_req{})
	-> {Value::string() | undefined, Req::#http_req{}}.
%% @equiv header(Name, Req) -> header(Name, Req, undefined)
header(Name, Req) ->
	header(Name, Req, undefined).

-spec header(Name::atom() | string(), Req::#http_req{}, Default)
	-> {Value::string() | Default, Req::#http_req{}} when Default::term().
header(Name, Req, Default) ->
	case lists:keyfind(Name, 1, Req#http_req.headers) of
		{Name, Value} -> {Value, Req};
		false -> {Default, Req}
	end.

-spec headers(Req::#http_req{})
	-> {list({Name::atom() | string(), Value::string()}), Req::#http_req{}}.
headers(Req) ->
	{Req#http_req.headers, Req}.

%% Request Body API.

%% @todo We probably want to allow a max length.
-spec body(Req::#http_req{})
	-> {ok, Body::binary(), Req::#http_req{}} | {error, Reason::posix()}.
body(Req) ->
	{Length, Req2} = cowboy_http_req:header('Content-Length', Req),
	case Length of
		undefined -> {error, badarg};
		_Any ->
			Length2 = list_to_integer(Length),
			body(Length2, Req2)
	end.

%% @todo We probably want to configure the timeout.
-spec body(Length::non_neg_integer(), Req::#http_req{})
	-> {ok, Body::binary(), Req::#http_req{}} | {error, Reason::posix()}.
body(Length, Req=#http_req{socket=Socket, transport=Transport,
		body_state=waiting}) ->
	Transport:setopts(Socket, [{packet, raw}]),
	case Transport:recv(Socket, Length, 5000) of
		{ok, Body} -> {ok, Body, Req#http_req{body_state=done}};
		{error, Reason} -> {error, Reason}
	end.

-spec body_qs(Req::#http_req{})
	-> {list({Name::string(), Value::string() | true}), Req::#http_req{}}.
body_qs(Req) ->
	{ok, Body, Req2} = body(Req),
	{parse_qs(binary_to_list(Body)), Req2}.

%% Response API.

-spec reply(Code::http_status(), Headers::http_headers(),
	Body::iolist(), Req::#http_req{}) -> {ok, Req::#http_req{}}.
%% @todo Don't be naive about the headers!
reply(Code, Headers, Body, Req=#http_req{socket=Socket,
		transport=Transport, connection=Connection,
		resp_state=waiting}) ->
	StatusLine = ["HTTP/1.1 ", status(Code), "\r\n"],
	BaseHeaders = ["Connection: ", atom_to_connection(Connection),
		"\r\nContent-Length: ", integer_to_list(iolist_size(Body)), "\r\n"],
	Transport:send(Socket,
		[StatusLine, BaseHeaders, Headers, "\r\n", Body]),
	{ok, Req#http_req{resp_state=done}}.

%% Internal.

-spec parse_qs(Qs::string()) -> list({Name::string(), Value::string() | true}).
parse_qs(Qs) ->
	Tokens = string:tokens(Qs, "&"),
	[case string:chr(Token, $=) of
		0 ->
			{Token, true};
		N ->
			{Name, [$=|Value]} = lists:split(N - 1, Token),
			{Name, Value}
	end || Token <- Tokens].

-spec atom_to_connection(Atom::keepalive | close) -> string().
atom_to_connection(keepalive) ->
	"keep-alive";
atom_to_connection(close) ->
	"close".

-spec status(Code::http_status()) -> string().
status(100) -> "100 Continue";
status(101) -> "101 Switching Protocols";
status(102) -> "102 Processing";
status(200) -> "200 OK";
status(201) -> "201 Created";
status(202) -> "202 Accepted";
status(203) -> "203 Non-Authoritative Information";
status(204) -> "204 No Content";
status(205) -> "205 Reset Content";
status(206) -> "206 Partial Content";
status(207) -> "207 Multi-Status";
status(226) -> "226 IM Used";
status(300) -> "300 Multiple Choices";
status(301) -> "301 Moved Permanently";
status(302) -> "302 Found";
status(303) -> "303 See Other";
status(304) -> "304 Not Modified";
status(305) -> "305 Use Proxy";
status(306) -> "306 Switch Proxy";
status(307) -> "307 Temporary Redirect";
status(400) -> "400 Bad Request";
status(401) -> "401 Unauthorized";
status(402) -> "402 Payment Required";
status(403) -> "403 Forbidden";
status(404) -> "404 Not Found";
status(405) -> "405 Method Not Allowed";
status(406) -> "406 Not Acceptable";
status(407) -> "407 Proxy Authentication Required";
status(408) -> "408 Request Timeout";
status(409) -> "409 Conflict";
status(410) -> "410 Gone";
status(411) -> "411 Length Required";
status(412) -> "412 Precondition Failed";
status(413) -> "413 Request Entity Too Large";
status(414) -> "414 Request-URI Too Long";
status(415) -> "415 Unsupported Media Type";
status(416) -> "416 Requested Range Not Satisfiable";
status(417) -> "417 Expectation Failed";
status(418) -> "418 I'm a teapot";
status(422) -> "422 Unprocessable Entity";
status(423) -> "423 Locked";
status(424) -> "424 Failed Dependency";
status(425) -> "425 Unordered Collection";
status(426) -> "426 Upgrade Required";
status(500) -> "500 Internal Server Error";
status(501) -> "501 Not Implemented";
status(502) -> "502 Bad Gateway";
status(503) -> "503 Service Unavailable";
status(504) -> "504 Gateway Timeout";
status(505) -> "505 HTTP Version Not Supported";
status(506) -> "506 Variant Also Negotiates";
status(507) -> "507 Insufficient Storage";
status(510) -> "510 Not Extended";
status(L) when is_list(L) -> L.

%% Tests.

-ifdef(TEST).

parse_qs_test_() ->
	%% {Qs, Result}
	Tests = [
		{"", []},
		{"a=b", [{"a", "b"}]},
		{"aaa=bbb", [{"aaa", "bbb"}]},
		{"a&b", [{"a", true}, {"b", true}]},
		{"a=b&c&d=e", [{"a", "b"}, {"c", true}, {"d", "e"}]},
		{"a=b=c=d=e&f=g", [{"a", "b=c=d=e"}, {"f", "g"}]}
	],
	[{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].

-endif.