diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cowboy.app.src | 2 | ||||
-rw-r--r-- | src/cowboy_http.erl | 29 | ||||
-rw-r--r-- | src/cowboy_http_protocol.erl | 15 | ||||
-rw-r--r-- | src/cowboy_http_req.erl | 112 | ||||
-rw-r--r-- | src/cowboy_http_rest.erl | 113 | ||||
-rw-r--r-- | src/cowboy_http_static.erl | 4 | ||||
-rw-r--r-- | src/cowboy_multipart.erl | 249 |
7 files changed, 452 insertions, 72 deletions
diff --git a/src/cowboy.app.src b/src/cowboy.app.src index 33cd876..9b3ee50 100644 --- a/src/cowboy.app.src +++ b/src/cowboy.app.src @@ -14,7 +14,7 @@ {application, cowboy, [ {description, "Small, fast, modular HTTP server."}, - {vsn, "0.4.0"}, + {vsn, "0.5.0"}, {modules, []}, {registered, [cowboy_clock, cowboy_sup]}, {applications, [ diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 99536a3..32b0ca9 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -17,15 +17,40 @@ -module(cowboy_http). %% Parsing. --export([list/2, nonempty_list/2, content_type/1, +-export([list/2, nonempty_list/2, content_type/1, content_type_params/3, media_range/2, conneg/2, language_range/2, entity_tag_match/1, http_date/1, rfc1123_date/1, rfc850_date/1, asctime_date/1, - digits/1, token/2, token_ci/2, quoted_string/2]). + whitespace/2, digits/1, token/2, token_ci/2, quoted_string/2]). %% Interpretation. -export([connection_to_atom/1, urldecode/1, urldecode/2, urlencode/1, urlencode/2]). +-type method() :: 'OPTIONS' | 'GET' | 'HEAD' + | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary(). +-type uri() :: '*' | {absoluteURI, http | https, Host::binary(), + Port::integer() | undefined, Path::binary()} + | {scheme, Scheme::binary(), binary()} + | {abs_path, binary()} | binary(). +-type version() :: {Major::non_neg_integer(), Minor::non_neg_integer()}. +-type header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma' + | 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset' + | 'Accept-Encoding' | 'Accept-Language' | 'Authorization' | 'From' | 'Host' + | 'If-Modified-Since' | 'If-Match' | 'If-None-Match' | 'If-Range' + | 'If-Unmodified-Since' | 'Max-Forwards' | 'Proxy-Authorization' | 'Range' + | 'Referer' | 'User-Agent' | 'Age' | 'Location' | 'Proxy-Authenticate' + | 'Public' | 'Retry-After' | 'Server' | 'Vary' | 'Warning' + | 'Www-Authenticate' | 'Allow' | 'Content-Base' | 'Content-Encoding' + | 'Content-Language' | 'Content-Length' | 'Content-Location' + | 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag' + | 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie' + | 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive' + | 'Proxy-Connection' | binary(). +-type headers() :: [{header(), iodata()}]. +-type status() :: non_neg_integer() | binary(). + +-export_type([method/0, uri/0, version/0, header/0, headers/0, status/0]). + -include("include/http.hrl"). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl index ea59799..a714111 100644 --- a/src/cowboy_http_protocol.erl +++ b/src/cowboy_http_protocol.erl @@ -108,8 +108,8 @@ wait_request(State=#state{socket=Socket, transport=Transport, {error, _Reason} -> terminate(State) end. --spec request({http_request, http_method(), http_uri(), - http_version()}, #state{}) -> ok | none(). +-spec request({http_request, cowboy_http:method(), cowboy_http:uri(), + cowboy_http:version()}, #state{}) -> ok | none(). request({http_request, _Method, _URI, Version}, State) when Version =/= {1, 0}, Version =/= {1, 1} -> error_terminate(505, State); @@ -158,7 +158,7 @@ wait_header(Req, State=#state{socket=Socket, {error, closed} -> terminate(State) end. --spec header({http_header, integer(), http_header(), any(), binary()} +-spec header({http_header, integer(), cowboy_http:header(), any(), binary()} | http_eoh, #http_req{}, #state{}) -> ok | none(). header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{ transport=Transport, host=undefined}, State) -> @@ -380,7 +380,10 @@ ensure_body_processed(Req=#http_req{body_state=waiting}) -> {error, badarg} -> ok; %% No body. {error, _Reason} -> close; _Any -> ok - end. + end; +ensure_body_processed(Req=#http_req{body_state={multipart, _, _}}) -> + {ok, Req2} = cowboy_http_req:multipart_skip(Req), + ensure_body_processed(Req2). -spec ensure_response(#http_req{}) -> ok. %% The handler has already fully replied to the client. @@ -400,7 +403,7 @@ ensure_response(#http_req{socket=Socket, transport=Transport, close. %% Only send an error reply if there is no resp_sent message. --spec error_terminate(http_status(), #state{}) -> ok. +-spec error_terminate(cowboy_http:status(), #state{}) -> ok. error_terminate(Code, State=#state{socket=Socket, transport=Transport}) -> receive {cowboy_http_req, resp_sent} -> ok @@ -419,7 +422,7 @@ terminate(#state{socket=Socket, transport=Transport}) -> %% Internal. --spec version_to_connection(http_version()) -> keepalive | close. +-spec version_to_connection(cowboy_http:version()) -> keepalive | close. version_to_connection({1, 1}) -> keepalive; version_to_connection(_Any) -> close. diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl index b0a0232..92d96ad 100644 --- a/src/cowboy_http_req.erl +++ b/src/cowboy_http_req.erl @@ -34,7 +34,8 @@ ]). %% Request API. -export([ - body/1, body/2, body_qs/1 + body/1, body/2, body_qs/1, + multipart_data/1, multipart_skip/1 ]). %% Request Body API. -export([ @@ -55,12 +56,12 @@ %% Request API. %% @doc Return the HTTP method of the request. --spec method(#http_req{}) -> {http_method(), #http_req{}}. +-spec method(#http_req{}) -> {cowboy_http:method(), #http_req{}}. method(Req) -> {Req#http_req.method, Req}. %% @doc Return the HTTP version used for the request. --spec version(#http_req{}) -> {http_version(), #http_req{}}. +-spec version(#http_req{}) -> {cowboy_http:version(), #http_req{}}. version(Req) -> {Req#http_req.version, Req}. @@ -208,7 +209,7 @@ header(Name, Req, Default) when is_atom(Name) orelse is_binary(Name) -> end. %% @doc Return the full list of headers. --spec headers(#http_req{}) -> {http_headers(), #http_req{}}. +-spec headers(#http_req{}) -> {cowboy_http:headers(), #http_req{}}. headers(Req) -> {Req#http_req.headers, Req}. @@ -217,7 +218,7 @@ headers(Req) -> %% When the value isn't found, a proper default value for the type %% returned is used as a return value. %% @see parse_header/3 --spec parse_header(http_header(), #http_req{}) +-spec parse_header(cowboy_http:header(), #http_req{}) -> {any(), #http_req{}} | {error, badarg}. parse_header(Name, Req=#http_req{p_headers=PHeaders}) -> case lists:keyfind(Name, 1, PHeaders) of @@ -226,14 +227,14 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}) -> end. %% @doc Default values for semantic header parsing. --spec parse_header_default(http_header()) -> any(). +-spec parse_header_default(cowboy_http:header()) -> any(). parse_header_default('Connection') -> []; parse_header_default(_Name) -> undefined. %% @doc Semantically parse headers. %% %% When the header is unknown, the value is returned directly without parsing. --spec parse_header(http_header(), #http_req{}, any()) +-spec parse_header(cowboy_http:header(), #http_req{}, any()) -> {any(), #http_req{}} | {error, badarg}. parse_header(Name, Req, Default) when Name =:= 'Accept' -> parse_header(Name, Req, Default, @@ -363,6 +364,7 @@ meta(Name, Req, Default) -> %% @doc Return the full body sent with the request, or <em>{error, badarg}</em> %% if no <em>Content-Length</em> is available. %% @todo We probably want to allow a max length. +%% @todo Add multipart support to this function. -spec body(#http_req{}) -> {ok, binary(), #http_req{}} | {error, atom()}. body(Req) -> {Length, Req2} = cowboy_http_req:parse_header('Content-Length', Req), @@ -400,6 +402,72 @@ body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) -> {ok, Body, Req2} = body(Req), {parse_qs(Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}. +%% Multipart Request API. + +%% @doc Return data from the multipart parser. +%% +%% Use this function for multipart streaming. For each part in the request, +%% this function returns <em>{headers, Headers}</em> followed by a sequence of +%% <em>{data, Data}</em> tuples and finally <em>end_of_part</em>. When there +%% is no part to parse anymore, <em>eof</em> is returned. +%% +%% If the request Content-Type is not a multipart one, <em>{error, badarg}</em> +%% is returned. +-spec multipart_data(#http_req{}) + -> {{headers, cowboy_http:headers()} + | {data, binary()} | end_of_part | eof, + #http_req{}}. +multipart_data(Req=#http_req{body_state=waiting}) -> + {{<<"multipart">>, _SubType, Params}, Req2} = + parse_header('Content-Type', Req), + {_, Boundary} = lists:keyfind(<<"boundary">>, 1, Params), + {Length, Req3=#http_req{buffer=Buffer}} = + parse_header('Content-Length', Req2), + multipart_data(Req3, Length, cowboy_multipart:parser(Boundary), Buffer); +multipart_data(Req=#http_req{body_state={multipart, Length, Cont}}) -> + multipart_data(Req, Length, Cont()); +multipart_data(Req=#http_req{body_state=done}) -> + {eof, Req}. + +multipart_data(Req, Length, Parser, Buffer) when byte_size(Buffer) >= Length -> + << Data:Length/binary, Rest/binary >> = Buffer, + multipart_data(Req#http_req{buffer=Rest}, 0, Parser(Data)); +multipart_data(Req, Length, Parser, Buffer) -> + NewLength = Length - byte_size(Buffer), + multipart_data(Req#http_req{buffer= <<>>}, NewLength, Parser(Buffer)). + +multipart_data(Req, Length, {headers, Headers, Cont}) -> + {{headers, Headers}, Req#http_req{body_state={multipart, Length, Cont}}}; +multipart_data(Req, Length, {body, Data, Cont}) -> + {{body, Data}, Req#http_req{body_state={multipart, Length, Cont}}}; +multipart_data(Req, Length, {end_of_part, Cont}) -> + {end_of_part, Req#http_req{body_state={multipart, Length, Cont}}}; +multipart_data(Req, 0, eof) -> + {eof, Req#http_req{body_state=done}}; +multipart_data(Req=#http_req{socket=Socket, transport=Transport}, + Length, eof) -> + {ok, _Data} = Transport:recv(Socket, Length, 5000), + {eof, Req#http_req{body_state=done}}; +multipart_data(Req=#http_req{socket=Socket, transport=Transport}, + Length, {more, Parser}) when Length > 0 -> + case Transport:recv(Socket, 0, 5000) of + {ok, << Data:Length/binary, Buffer/binary >>} -> + multipart_data(Req#http_req{buffer=Buffer}, 0, Parser(Data)); + {ok, Data} -> + multipart_data(Req, Length - byte_size(Data), Parser(Data)) + end. + +%% @doc Skip a part returned by the multipart parser. +%% +%% This function repeatedly calls <em>multipart_data/1</em> until +%% <em>end_of_part</em> or <em>eof</em> is parsed. +multipart_skip(Req) -> + case multipart_data(Req) of + {end_of_part, Req2} -> {ok, Req2}; + {eof, Req2} -> {ok, Req2}; + {_Other, Req2} -> multipart_skip(Req2) + end. + %% Response API. %% @doc Add a cookie header to the response. @@ -410,7 +478,7 @@ set_resp_cookie(Name, Value, Options, Req) -> set_resp_header(HeaderName, HeaderValue, Req). %% @doc Add a header to the response. --spec set_resp_header(http_header(), iodata(), #http_req{}) +-spec set_resp_header(cowboy_http:header(), iodata(), #http_req{}) -> {ok, #http_req{}}. set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) -> NameBin = header_to_binary(Name), @@ -447,7 +515,7 @@ set_resp_body_fun(StreamLen, StreamFun, Req) -> %% @doc Return whether the given header has been set for the response. --spec has_resp_header(http_header(), #http_req{}) -> boolean(). +-spec has_resp_header(cowboy_http:header(), #http_req{}) -> boolean(). has_resp_header(Name, #http_req{resp_headers=RespHeaders}) -> NameBin = header_to_binary(Name), lists:keymember(NameBin, 1, RespHeaders). @@ -460,17 +528,18 @@ has_resp_body(#http_req{resp_body=RespBody}) -> iolist_size(RespBody) > 0. %% @equiv reply(Status, [], [], Req) --spec reply(http_status(), #http_req{}) -> {ok, #http_req{}}. +-spec reply(cowboy_http:status(), #http_req{}) -> {ok, #http_req{}}. reply(Status, Req=#http_req{resp_body=Body}) -> reply(Status, [], Body, Req). %% @equiv reply(Status, Headers, [], Req) --spec reply(http_status(), http_headers(), #http_req{}) -> {ok, #http_req{}}. +-spec reply(cowboy_http:status(), cowboy_http:headers(), #http_req{}) + -> {ok, #http_req{}}. reply(Status, Headers, Req=#http_req{resp_body=Body}) -> reply(Status, Headers, Body, Req). %% @doc Send a reply to the client. --spec reply(http_status(), http_headers(), iodata(), #http_req{}) +-spec reply(cowboy_http:status(), cowboy_http:headers(), iodata(), #http_req{}) -> {ok, #http_req{}}. reply(Status, Headers, Body, Req=#http_req{socket=Socket, transport=Transport, connection=Connection, pid=ReqPid, @@ -493,13 +562,13 @@ reply(Status, Headers, Body, Req=#http_req{socket=Socket, resp_headers=[], resp_body= <<>>}}. %% @equiv chunked_reply(Status, [], Req) --spec chunked_reply(http_status(), #http_req{}) -> {ok, #http_req{}}. +-spec chunked_reply(cowboy_http:status(), #http_req{}) -> {ok, #http_req{}}. chunked_reply(Status, Req) -> chunked_reply(Status, [], Req). %% @doc Initiate the sending of a chunked reply to the client. %% @see cowboy_http_req:chunk/2 --spec chunked_reply(http_status(), http_headers(), #http_req{}) +-spec chunked_reply(cowboy_http:status(), cowboy_http:headers(), #http_req{}) -> {ok, #http_req{}}. chunked_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport, connection=Connection, pid=ReqPid, @@ -528,7 +597,7 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) -> %% @doc Send an upgrade reply. %% @private --spec upgrade_reply(http_status(), http_headers(), #http_req{}) +-spec upgrade_reply(cowboy_http:status(), cowboy_http:headers(), #http_req{}) -> {ok, #http_req{}}. upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport, pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) -> @@ -578,7 +647,7 @@ parse_qs(Qs, URLDecode) -> [Name, Value] -> {URLDecode(Name), URLDecode(Value)} end || Token <- Tokens]. --spec response_connection(http_headers(), keepalive | close) +-spec response_connection(cowboy_http:headers(), keepalive | close) -> keepalive | close. response_connection([], Connection) -> Connection; @@ -599,8 +668,8 @@ response_connection_parse(ReplyConn) -> Tokens = cowboy_http:nonempty_list(ReplyConn, fun cowboy_http:token/2), cowboy_http:connection_to_atom(Tokens). --spec response_head(http_status(), http_headers(), http_headers(), - http_headers()) -> iolist(). +-spec response_head(cowboy_http:status(), cowboy_http:headers(), + cowboy_http:headers(), cowboy_http:headers()) -> iolist(). response_head(Status, Headers, RespHeaders, DefaultHeaders) -> StatusLine = <<"HTTP/1.1 ", (status(Status))/binary, "\r\n">>, Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers], @@ -611,7 +680,8 @@ response_head(Status, Headers, RespHeaders, DefaultHeaders) -> || {Key, Value} <- Headers3], [StatusLine, Headers4, <<"\r\n">>]. --spec merge_headers(http_headers(), http_headers()) -> http_headers(). +-spec merge_headers(cowboy_http:headers(), cowboy_http:headers()) + -> cowboy_http:headers(). merge_headers(Headers, []) -> Headers; merge_headers(Headers, [{Name, Value}|Tail]) -> @@ -628,7 +698,7 @@ atom_to_connection(keepalive) -> atom_to_connection(close) -> <<"close">>. --spec status(http_status()) -> binary(). +-spec status(cowboy_http:status()) -> binary(). status(100) -> <<"100 Continue">>; status(101) -> <<"101 Switching Protocols">>; status(102) -> <<"102 Processing">>; @@ -684,7 +754,7 @@ status(507) -> <<"507 Insufficient Storage">>; status(510) -> <<"510 Not Extended">>; status(B) when is_binary(B) -> B. --spec header_to_binary(http_header()) -> binary(). +-spec header_to_binary(cowboy_http:header()) -> binary(). header_to_binary('Cache-Control') -> <<"Cache-Control">>; header_to_binary('Connection') -> <<"Connection">>; header_to_binary('Date') -> <<"Date">>; diff --git a/src/cowboy_http_rest.erl b/src/cowboy_http_rest.erl index 35f82e3..e6cc6ff 100644 --- a/src/cowboy_http_rest.erl +++ b/src/cowboy_http_rest.erl @@ -53,7 +53,8 @@ %% You do not need to call this function manually. To upgrade to the REST %% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em> %% in your <em>cowboy_http_handler:init/3</em> handler function. --spec upgrade(pid(), module(), any(), #http_req{}) -> {ok, #http_req{}}. +-spec upgrade(pid(), module(), any(), #http_req{}) + -> {ok, #http_req{}} | close. upgrade(_ListenerPid, Handler, Opts, Req) -> try case erlang:function_exported(Handler, rest_init, 2) of @@ -73,7 +74,7 @@ upgrade(_ListenerPid, Handler, Opts, Req) -> "** Request was ~p~n** Stacktrace: ~p~n~n", [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]), {ok, _Req2} = cowboy_http_req:reply(500, Req), - ok + close end. service_available(Req, State) -> @@ -88,8 +89,10 @@ known_methods(Req=#http_req{method=Method}, State) -> next(Req, State, fun uri_too_long/2); no_call -> next(Req, State, 501); - {List, Req2, HandlerState2} -> - State2 = State#state{handler_state=HandlerState2}, + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {List, Req2, HandlerState} -> + State2 = State#state{handler_state=HandlerState}, case lists:member(Method, List) of true -> next(Req2, State2, fun uri_too_long/2); false -> next(Req2, State2, 501) @@ -106,8 +109,10 @@ allowed_methods(Req=#http_req{method=Method}, State) -> next(Req, State, fun malformed_request/2); no_call -> method_not_allowed(Req, State, ['GET', 'HEAD']); - {List, Req2, HandlerState2} -> - State2 = State#state{handler_state=HandlerState2}, + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {List, Req2, HandlerState} -> + State2 = State#state{handler_state=HandlerState}, case lists:member(Method, List) of true -> next(Req2, State2, fun malformed_request/2); false -> method_not_allowed(Req2, State2, List) @@ -137,12 +142,14 @@ is_authorized(Req, State) -> case call(Req, State, is_authorized) of no_call -> forbidden(Req, State); - {true, Req2, HandlerState2} -> - forbidden(Req2, State#state{handler_state=HandlerState2}); - {{false, AuthHead}, Req2, HandlerState2} -> + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {true, Req2, HandlerState} -> + forbidden(Req2, State#state{handler_state=HandlerState}); + {{false, AuthHead}, Req2, HandlerState} -> {ok, Req3} = cowboy_http_req:set_resp_header( <<"Www-Authenticate">>, AuthHead, Req2), - respond(Req3, State#state{handler_state=HandlerState2}, 401) + respond(Req3, State#state{handler_state=HandlerState}, 401) end. forbidden(Req, State) -> @@ -162,8 +169,12 @@ valid_entity_length(Req, State) -> %% If you need to add additional headers to the response at this point, %% you should do it directly in the options/2 call using set_resp_headers. options(Req=#http_req{method='OPTIONS'}, State) -> - {ok, Req2, HandlerState2} = call(Req, State, options), - respond(Req2, State#state{handler_state=HandlerState2}, 200); + case call(Req, State, options) of + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {ok, Req2, HandlerState} -> + respond(Req2, State#state{handler_state=HandlerState}, 200) + end; options(Req, State) -> content_types_provided(Req, State). @@ -186,6 +197,8 @@ content_types_provided(Req=#http_req{meta=Meta}, State) -> case call(Req, State, content_types_provided) of no_call -> not_acceptable(Req, State); + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); {[], Req2, HandlerState} -> not_acceptable(Req2, State#state{handler_state=HandlerState}); {CTP, Req2, HandlerState} -> @@ -280,10 +293,12 @@ languages_provided(Req, State) -> case call(Req, State, languages_provided) of no_call -> charsets_provided(Req, State); - {[], Req2, HandlerState2} -> - not_acceptable(Req2, State#state{handler_state=HandlerState2}); - {LP, Req2, HandlerState2} -> - State2 = State#state{handler_state=HandlerState2, languages_p=LP}, + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {[], Req2, HandlerState} -> + not_acceptable(Req2, State#state{handler_state=HandlerState}); + {LP, Req2, HandlerState} -> + State2 = State#state{handler_state=HandlerState, languages_p=LP}, {AcceptLanguage, Req3} = cowboy_http_req:parse_header('Accept-Language', Req2), case AcceptLanguage of @@ -341,10 +356,12 @@ charsets_provided(Req, State) -> case call(Req, State, charsets_provided) of no_call -> set_content_type(Req, State); - {[], Req2, HandlerState2} -> - not_acceptable(Req2, State#state{handler_state=HandlerState2}); - {CP, Req2, HandlerState2} -> - State2 = State#state{handler_state=HandlerState2, charsets_p=CP}, + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {[], Req2, HandlerState} -> + not_acceptable(Req2, State#state{handler_state=HandlerState}); + {CP, Req2, HandlerState} -> + State2 = State#state{handler_state=HandlerState, charsets_p=CP}, {AcceptCharset, Req3} = cowboy_http_req:parse_header('Accept-Charset', Req2), case AcceptCharset of @@ -579,12 +596,14 @@ is_put_to_missing_resource(Req, State) -> %% with Location the full new URI of the resource. moved_permanently(Req, State, OnFalse) -> case call(Req, State, moved_permanently) of - {{true, Location}, Req2, HandlerState2} -> + {{true, Location}, Req2, HandlerState} -> {ok, Req3} = cowboy_http_req:set_resp_header( <<"Location">>, Location, Req2), - respond(Req3, State#state{handler_state=HandlerState2}, 301); - {false, Req2, HandlerState2} -> - OnFalse(Req2, State#state{handler_state=HandlerState2}); + respond(Req3, State#state{handler_state=HandlerState}, 301); + {false, Req2, HandlerState} -> + OnFalse(Req2, State#state{handler_state=HandlerState}); + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); no_call -> OnFalse(Req, State) end. @@ -598,12 +617,14 @@ previously_existed(Req, State) -> %% with Location the full new URI of the resource. moved_temporarily(Req, State) -> case call(Req, State, moved_temporarily) of - {{true, Location}, Req2, HandlerState2} -> + {{true, Location}, Req2, HandlerState} -> {ok, Req3} = cowboy_http_req:set_resp_header( <<"Location">>, Location, Req2), - respond(Req3, State#state{handler_state=HandlerState2}, 307); - {false, Req2, HandlerState2} -> - is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410); + respond(Req3, State#state{handler_state=HandlerState}, 307); + {false, Req2, HandlerState} -> + is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState}, 410); + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); no_call -> is_post_to_missing_resource(Req, State, 410) end. @@ -642,6 +663,8 @@ post_is_create(Req, State) -> %% (including the leading /). create_path(Req=#http_req{meta=Meta}, State) -> case call(Req, State, create_path) of + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); {Path, Req2, HandlerState} -> Location = create_path_location(Req2, Path), State2 = State#state{handler_state=HandlerState}, @@ -672,6 +695,8 @@ create_path_location_port(_, Port) -> %% and false when it hasn't, in which case a 500 error is sent. process_post(Req, State) -> case call(Req, State, process_post) of + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); {true, Req2, HandlerState} -> State2 = State#state{handler_state=HandlerState}, next(Req2, State2, 201); @@ -699,8 +724,10 @@ put_resource(Req, State, OnTrue) -> case call(Req, State, content_types_accepted) of no_call -> respond(Req, State, 415); - {CTA, Req2, HandlerState2} -> - State2 = State#state{handler_state=HandlerState2}, + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {CTA, Req2, HandlerState} -> + State2 = State#state{handler_state=HandlerState}, {ContentType, Req3} = cowboy_http_req:parse_header('Content-Type', Req2), choose_content_type(Req3, State2, OnTrue, ContentType, CTA) @@ -711,6 +738,8 @@ choose_content_type(Req, State, _OnTrue, _ContentType, []) -> choose_content_type(Req, State, OnTrue, ContentType, [{Accepted, Fun}|_Tail]) when ContentType =:= Accepted -> case call(Req, State, Fun) of + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); {true, Req2, HandlerState} -> State2 = State#state{handler_state=HandlerState}, next(Req2, State2, OnTrue); @@ -754,6 +783,8 @@ set_resp_body(Req=#http_req{method=Method}, end, {Req5, State4} = set_resp_expires(Req4, State3), case call(Req5, State4, Fun) of + {halt, Req6, HandlerState} -> + terminate(Req6, State4#state{handler_state=HandlerState}); {Body, Req6, HandlerState} -> State5 = State4#state{handler_state=HandlerState}, {ok, Req7} = case Body of @@ -803,8 +834,8 @@ generate_etag(Req, State=#state{etag=undefined}) -> case call(Req, State, generate_etag) of no_call -> {undefined, Req, State#state{etag=no_call}}; - {Etag, Req2, HandlerState2} -> - {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}} + {Etag, Req2, HandlerState} -> + {Etag, Req2, State#state{handler_state=HandlerState, etag=Etag}} end; generate_etag(Req, State=#state{etag=Etag}) -> {Etag, Req, State}. @@ -815,8 +846,8 @@ last_modified(Req, State=#state{last_modified=undefined}) -> case call(Req, State, last_modified) of no_call -> {undefined, Req, State#state{last_modified=no_call}}; - {LastModified, Req2, HandlerState2} -> - {LastModified, Req2, State#state{handler_state=HandlerState2, + {LastModified, Req2, HandlerState} -> + {LastModified, Req2, State#state{handler_state=HandlerState, last_modified=LastModified}} end; last_modified(Req, State=#state{last_modified=LastModified}) -> @@ -828,8 +859,8 @@ expires(Req, State=#state{expires=undefined}) -> case call(Req, State, expires) of no_call -> {undefined, Req, State#state{expires=no_call}}; - {Expires, Req2, HandlerState2} -> - {Expires, Req2, State#state{handler_state=HandlerState2, + {Expires, Req2, HandlerState} -> + {Expires, Req2, State#state{handler_state=HandlerState, expires=Expires}} end; expires(Req, State=#state{expires=Expires}) -> @@ -841,10 +872,12 @@ expect(Req, State, Callback, Expected, OnTrue, OnFalse) -> case call(Req, State, Callback) of no_call -> next(Req, State, OnTrue); - {Expected, Req2, HandlerState2} -> - next(Req2, State#state{handler_state=HandlerState2}, OnTrue); - {_Unexpected, Req2, HandlerState2} -> - next(Req2, State#state{handler_state=HandlerState2}, OnFalse) + {halt, Req2, HandlerState} -> + terminate(Req2, State#state{handler_state=HandlerState}); + {Expected, Req2, HandlerState} -> + next(Req2, State#state{handler_state=HandlerState}, OnTrue); + {_Unexpected, Req2, HandlerState} -> + next(Req2, State#state{handler_state=HandlerState}, OnFalse) end. call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) -> diff --git a/src/cowboy_http_static.erl b/src/cowboy_http_static.erl index 3e3cb9e..0ee996a 100644 --- a/src/cowboy_http_static.erl +++ b/src/cowboy_http_static.erl @@ -153,7 +153,7 @@ -type dirpath() :: string() | binary() | [binary()]. -type dirspec() :: dirpath() | {priv, atom(), dirpath()}. -type mimedef() :: {binary(), binary(), [{binary(), binary()}]}. --type etagarg() :: {filepath, binary()} | {mtime, cowboy_clock:datetime()} +-type etagarg() :: {filepath, binary()} | {mtime, calendar:datetime()} | {inode, non_neg_integer()} | {filesize, non_neg_integer()}. %% handler state @@ -240,7 +240,7 @@ forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) -> %% @private Read the time a file system system object was last modified. -spec last_modified(#http_req{}, #state{}) -> - {cowboy_clock:datetime(), #http_req{}, #state{}}. + {calendar:datetime(), #http_req{}, #state{}}. last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) -> {Modified, Req, State}. diff --git a/src/cowboy_multipart.erl b/src/cowboy_multipart.erl new file mode 100644 index 0000000..b7aeb54 --- /dev/null +++ b/src/cowboy_multipart.erl @@ -0,0 +1,249 @@ +%% 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. + +%% @doc Multipart parser. +-module(cowboy_multipart). + +-type part_parser() :: parser(more(part_result())). +-type parser(T) :: fun((binary()) -> T). +-type more(T) :: T | {more, parser(T)}. +-type part_result() :: headers() | eof. +-type headers() :: {headers, http_headers(), body_cont()}. +-type http_headers() :: [{atom() | binary(), binary()}]. +-type body_cont() :: cont(more(body_result())). +-type cont(T) :: fun(() -> T). +-type body_result() :: {body, binary(), body_cont()} | end_of_part(). +-type end_of_part() :: {end_of_part, cont(more(part_result()))}. +-type disposition() :: {binary(), [{binary(), binary()}]}. + +-export([parser/1, content_disposition/1]). + +-include_lib("eunit/include/eunit.hrl"). + +%% API. + +%% @doc Return a multipart parser for the given boundary. +-spec parser(binary()) -> part_parser(). +parser(Boundary) when is_binary(Boundary) -> + fun (Bin) when is_binary(Bin) -> parse(Bin, Boundary) end. + +%% @doc Parse a content disposition. +%% @todo Parse the MIME header instead of the HTTP one. +-spec content_disposition(binary()) -> disposition(). +content_disposition(Data) -> + cowboy_http:token_ci(Data, + fun (_Rest, <<>>) -> {error, badarg}; + (Rest, Disposition) -> + cowboy_http:content_type_params(Rest, + fun (Params) -> {Disposition, Params} end, []) + end). + +%% Internal. + +%% @doc Entry point of the multipart parser, skips over the preamble if any. +-spec parse(binary(), binary()) -> more(part_result()). +parse(Bin, Boundary) when byte_size(Bin) >= byte_size(Boundary) + 2 -> + BoundarySize = byte_size(Boundary), + Pattern = pattern(Boundary), + case Bin of + <<"--", Boundary:BoundarySize/binary, Rest/binary>> -> + % Data starts with initial boundary, skip preamble parsing. + parse_boundary_tail(Rest, Pattern); + _ -> + % Parse preamble. + skip(Bin, Pattern) + end; +parse(Bin, Boundary) -> + % Not enough data to know if the data begins with a boundary. + more(Bin, fun (NewBin) -> parse(NewBin, Boundary) end). + +-type pattern() :: {binary:cp(), non_neg_integer()}. + +%% @doc Return a compiled binary pattern with its size in bytes. +%% The pattern is the boundary prepended with "\r\n--". +-spec pattern(binary()) -> pattern(). +pattern(Boundary) -> + MatchPattern = <<"\r\n--", Boundary/binary>>, + {binary:compile_pattern(MatchPattern), byte_size(MatchPattern)}. + +%% @doc Parse remaining characters of a line beginning with the boundary. +%% If followed by "--", <em>eof</em> is returned and parsing is finished. +-spec parse_boundary_tail(binary(), pattern()) -> more(part_result()). +parse_boundary_tail(Bin, Pattern) when byte_size(Bin) >= 2 -> + case Bin of + <<"--", _Rest/binary>> -> + % Boundary is followed by "--", end parsing. + eof; + _ -> + % No dash after boundary, proceed with unknown chars and lwsp + % removal. + parse_boundary_eol(Bin, Pattern) + end; +parse_boundary_tail(Bin, Pattern) -> + % Boundary may be followed by "--", need more data. + more(Bin, fun (NewBin) -> parse_boundary_tail(NewBin, Pattern) end). + +%% @doc Skip whitespace and unknown chars until CRLF. +-spec parse_boundary_eol(binary(), pattern()) -> more(part_result()). +parse_boundary_eol(Bin, Pattern) -> + case binary:match(Bin, <<"\r\n">>) of + {CrlfStart, _Length} -> + % End of line found, remove optional whitespace. + <<_:CrlfStart/binary, Rest/binary>> = Bin, + Fun = fun (Rest2) -> parse_boundary_crlf(Rest2, Pattern) end, + cowboy_http:whitespace(Rest, Fun); + nomatch -> + % CRLF not found in the given binary. + RestStart = max(byte_size(Bin) - 1, 0), + <<_:RestStart/binary, Rest/binary>> = Bin, + more(Rest, fun (NewBin) -> parse_boundary_eol(NewBin, Pattern) end) + end. + +-spec parse_boundary_crlf(binary(), pattern()) -> more(part_result()). +parse_boundary_crlf(<<"\r\n", Rest/binary>>, Pattern) -> + % The binary is at least 2 bytes long as this function is only called by + % parse_boundary_eol/3 when CRLF has been found so a more tuple will never + % be returned from here. + parse_headers(Rest, Pattern); +parse_boundary_crlf(Bin, Pattern) -> + % Unspecified behaviour here: RFC 2046 doesn't say what to do when LWSP is + % not followed directly by a new line. In this implementation it is + % considered part of the boundary so EOL needs to be searched again. + parse_boundary_eol(Bin, Pattern). + +-spec parse_headers(binary(), pattern()) -> more(part_result()). +parse_headers(Bin, Pattern) -> + parse_headers(Bin, Pattern, []). + +-spec parse_headers(binary(), pattern(), http_headers()) -> more(part_result()). +parse_headers(Bin, Pattern, Acc) -> + case erlang:decode_packet(httph_bin, Bin, []) of + {ok, {http_header, _, Name, _, Value}, Rest} -> + parse_headers(Rest, Pattern, [{Name, Value} | Acc]); + {ok, http_eoh, Rest} -> + Headers = lists:reverse(Acc), + {headers, Headers, fun () -> parse_body(Rest, Pattern) end}; + {ok, {http_error, _}, _} -> + % Skip malformed parts. + skip(Bin, Pattern); + {more, _} -> + more(Bin, fun (NewBin) -> parse_headers(NewBin, Pattern, Acc) end) + end. + +-spec parse_body(binary(), pattern()) -> more(body_result()). +parse_body(Bin, Pattern = {P, PSize}) when byte_size(Bin) >= PSize -> + case binary:match(Bin, P) of + {0, _Length} -> + <<_:PSize/binary, Rest/binary>> = Bin, + end_of_part(Rest, Pattern); + {BoundaryStart, _Length} -> + % Boundary found, this is the latest partial body that will be + % returned for this part. + <<PBody:BoundaryStart/binary, _:PSize/binary, Rest/binary>> = Bin, + FResult = end_of_part(Rest, Pattern), + {body, PBody, fun () -> FResult end}; + nomatch -> + PartialLength = byte_size(Bin) - PSize + 1, + <<PBody:PartialLength/binary, Rest/binary>> = Bin, + {body, PBody, fun () -> parse_body(Rest, Pattern) end} + end; +parse_body(Bin, Pattern) -> + more(Bin, fun (NewBin) -> parse_body(NewBin, Pattern) end). + +-spec end_of_part(binary(), pattern()) -> end_of_part(). +end_of_part(Bin, Pattern) -> + {end_of_part, fun () -> parse_boundary_tail(Bin, Pattern) end}. + +-spec skip(binary(), pattern()) -> more(part_result()). +skip(Bin, Pattern = {P, PSize}) -> + case binary:match(Bin, P) of + {BoundaryStart, _Length} -> + % Boundary found, proceed with parsing of the next part. + RestStart = BoundaryStart + PSize, + <<_:RestStart/binary, Rest/binary>> = Bin, + parse_boundary_tail(Rest, Pattern); + nomatch -> + % Boundary not found, need more data. + RestStart = max(byte_size(Bin) - PSize + 1, 0), + <<_:RestStart/binary, Rest/binary>> = Bin, + more(Rest, fun (NewBin) -> skip(NewBin, Pattern) end) + end. + +-spec more(binary(), parser(T)) -> {more, parser(T)}. +more(<<>>, F) -> + {more, F}; +more(Bin, InnerF) -> + F = fun (NewData) when is_binary(NewData) -> + InnerF(<<Bin/binary, NewData/binary>>) + end, + {more, F}. + +%% Tests. + +-ifdef(TEST). + +multipart_test_() -> + %% {Body, Result} + Tests = [ + {<<"--boundary--">>, []}, + {<<"preamble\r\n--boundary--">>, []}, + {<<"--boundary--\r\nepilogue">>, []}, + {<<"\r\n--boundary\r\nA:b\r\nC:d\r\n\r\n\r\n--boundary--">>, + [{[{<<"A">>, <<"b">>}, {<<"C">>, <<"d">>}], <<>>}]}, + { + << + "--boundary\r\nX-Name:answer\r\n\r\n42" + "\r\n--boundary\r\nServer:Cowboy\r\n\r\nIt rocks!\r\n" + "\r\n--boundary--" + >>, + [ + {[{<<"X-Name">>, <<"answer">>}], <<"42">>}, + {[{'Server', <<"Cowboy">>}], <<"It rocks!\r\n">>} + ] + } + ], + [{title(V), fun () -> R = acc_multipart(V) end} || {V, R} <- Tests]. + +acc_multipart(V) -> + acc_multipart((parser(<<"boundary">>))(V), []). + +acc_multipart({headers, Headers, Cont}, Acc) -> + acc_multipart(Cont(), [{Headers, []}|Acc]); +acc_multipart({body, Body, Cont}, [{Headers, BodyAcc}|Acc]) -> + acc_multipart(Cont(), [{Headers, [Body|BodyAcc]}|Acc]); +acc_multipart({end_of_part, Cont}, [{Headers, BodyAcc}|Acc]) -> + Body = list_to_binary(lists:reverse(BodyAcc)), + acc_multipart(Cont(), [{Headers, Body}|Acc]); +acc_multipart(eof, Acc) -> + lists:reverse(Acc). + +content_disposition_test_() -> + %% {Disposition, Result} + Tests = [ + {<<"form-data; name=id">>, {<<"form-data">>, [{<<"name">>, <<"id">>}]}}, + {<<"inline">>, {<<"inline">>, []}}, + {<<"attachment; \tfilename=brackets-slides.pdf">>, + {<<"attachment">>, [{<<"filename">>, <<"brackets-slides.pdf">>}]}} + ], + [{title(V), fun () -> R = content_disposition(V) end} || {V, R} <- Tests]. + +title(Bin) -> + Title = lists:foldl( + fun ({T, R}, V) -> re:replace(V, T, R, [global]) end, + Bin, + [{"\t", "\\\\t"}, {"\r", "\\\\r"}, {"\n", "\\\\n"}] + ), + iolist_to_binary(Title). + +-endif. |