Created
September 6, 2011 10:00
-
-
Save asabil/1197162 to your computer and use it in GitHub Desktop.
Cowboy HTTP REST
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(cowboy_http_rest). | |
-behaviour(cowboy_http_handler). | |
-export([ | |
init/3, | |
handle/2, | |
terminate/2 | |
]). | |
-export([ | |
behaviour_info/1 | |
]). | |
behaviour_info(callbacks) -> [ | |
{init, 3}, | |
%{service_available, 2}, | |
%{known_methods, 2}, | |
%{uri_too_long, 2}, | |
%{allowed_methods, 2}, | |
%{malformed_request, 2}, | |
%{is_authorized, 2}, | |
%{forbidden, 2}, | |
%{valid_content_headers, 2}, | |
%{valid_entity_length, 2}, | |
%{options, 2}, | |
{content_types_provided, 2}, | |
%{content_types_accepted, 2}, | |
%{languages_provided, 2}, | |
%{charsets_provided, 2}, | |
%{encodings_provided, 2}, | |
%{resource_exists, 2}, | |
%{generate_etag, 2}, | |
%{last_modified, 2}, | |
%{moved_permanently, 2}, | |
%{previously_existed, 2}, | |
%{moved_temporarily, 2}, | |
%{allow_missing_post, 2}, | |
%{delete_resource, 2}, | |
%{post_is_create, 2}, | |
%{process_post, 2}, | |
%{is_conflict, 2}, | |
%{multiple_choices, 2}, | |
{terminate, 2} | |
]; | |
behaviour_info(_Other) -> | |
undefined. | |
-include_lib("cowboy/include/http.hrl"). | |
-type resource() :: module(). | |
-type resource_state() :: term(). | |
-type http_content() :: file_content() | data_content(). | |
-type file_content() :: {file, Path :: binary()}. | |
-type data_content() :: {data, Data :: binary()}. | |
-type http_response() :: http_content() | {http_headers(), http_content()}. | |
-type content_provider() :: fun((#http_req{}, resource_state()) -> {http_response(), #http_req{}, resource_state()}). | |
-type content_encoder() :: fun((http_content()) -> http_content()). | |
-record(state, { | |
resource :: resource(), | |
resource_state :: resource_state(), | |
content_type :: undefined | {ContentType :: binary(), content_provider()}, | |
content_charset :: undefined | {ContentCharset :: binary(), content_encoder()}, | |
content_encoding :: undefined | {ContentEncoding :: binary(), content_encoder()}, | |
variances = [] :: [atom()], | |
response_status = 599 :: atom() | 100..600, | |
response_headers = [] :: http_headers(), | |
response_body :: undefined | http_content() | |
}). | |
%% @hidden | |
init(Transport, Req, {Resource, Opts}) -> | |
{ok, Req1, ResourceState} = Resource:init(Transport, Req, Opts), | |
{ok, Req1, #state{resource=Resource, resource_state=ResourceState}}. | |
%% @hidden | |
handle(Req, State) -> | |
try b13(Req, State) of | |
{Req2, State2} -> reply(Req2, State2) | |
catch | |
throw:{reply, Req2, State2} -> | |
reply(Req2, State2); | |
exit:{not_implemented, Req2, State2} -> | |
reply(Req2, State2#state{response_status = 501}) | |
end. | |
%% @hidden | |
terminate(Req, #state{resource=Resource, resource_state=ResourceState}) -> | |
Resource:terminate(Req, ResourceState). | |
%% @private | |
reply(Req, State, StatusCode, Headers) -> | |
throw({reply, Req, State#state{ | |
response_status = StatusCode, | |
response_headers = Headers ++ State#state.response_headers | |
}}). | |
%% @private | |
reply(Req, State, StatusCode) -> | |
throw({reply, Req, State#state{ | |
response_status = StatusCode | |
}}). | |
%% @private | |
reply(Req, State) -> | |
{Req1, State1} = finalize_content({Req, State}), | |
Status = State1#state.response_status, | |
Headers = finalize_headers(State1#state.response_headers), | |
{ok, ReqDone} = case State1#state.response_body of | |
undefined -> | |
cowboy_http_req:reply(Status, Headers, <<>>, Req1); | |
{data, Data} -> | |
cowboy_http_req:reply(Status, Headers, Data, Req1); | |
{file, Path} -> | |
case file:read_file(Path) of | |
{ok, Data} -> | |
cowboy_http_req:reply(Status, Headers, Data, Req1); | |
{error, _Reason} -> | |
cowboy_http_req:reply(404, [], <<>>, Req1) | |
end | |
end, | |
{ok, ReqDone, State1}. | |
%% @private | |
finalize_content({Req, #state{response_body = undefined} = State}) -> | |
{Req, State}; | |
finalize_content({Req, State}) -> | |
finalize_content_charset(case get_response_header('Content-Type', Req, State) of | |
undefined -> add_response_header({'Content-Type', <<"application/octet-stream">>}, {Req, State}); | |
_ -> {Req, State} | |
end). | |
finalize_content_charset({Req, #state{content_charset = {<<"*">>, _}} = State}) -> | |
finalize_content_encoding({Req, State}); | |
finalize_content_charset({Req, #state{content_charset = {Charset, Encoder}} = State}) -> | |
{_, Type} = get_response_header('Content-Type', Req, State), | |
Header = {'Content-Type', <<Type/binary, "; charset=", Charset/binary>>}, | |
finalize_content_encoding( | |
add_response_header(Header, {Req, State#state{response_body = Encoder(State#state.response_body)}}) | |
). | |
finalize_content_encoding({Req, #state{content_encoding = {Encoding, Encoder}} = State}) -> | |
Header = {'Content-Encoding', Encoding}, | |
add_response_header(Header, {Req, State#state{response_body = Encoder(State#state.response_body)}}). | |
%% @private | |
finalize_headers(Headers) -> | |
lists:ukeysort(1, Headers). | |
%% @private | |
add_response_header(Header, {Req, State}) -> | |
{Req, State#state{response_headers = [Header | State#state.response_headers]}}. | |
%% @private | |
add_response_headers(Headers, {Req, State}) -> | |
{Req, State#state{response_headers = Headers ++ State#state.response_headers}}. | |
%% @private | |
get_response_header(Name, _Req, State) -> | |
case lists:keyfind(Name, 1, State#state.response_headers) of | |
false -> undefined; | |
Header -> Header | |
end. | |
%% @private | |
check_resource(Req, State, Callback, Expected, OnTrue, OnFalse) when is_function(Expected) -> | |
case call(Req, State, Callback) of | |
no_call -> next(Req, State, no_call, OnTrue); | |
{Value, Req2, State2} -> | |
case Expected(Value) of | |
true -> next(Req2, State2, Value, OnTrue); | |
false -> next(Req2, State2, Value, OnFalse) | |
end | |
end; | |
check_resource(Req, State, Callback, Expected, OnTrue, OnFalse) -> | |
check_resource(Req, State, Callback, fun(Value) -> | |
Value =:= Expected | |
end, OnTrue, OnFalse). | |
%% @private | |
check_header(Req, State, Header, OnDefined, OnUndefined) -> | |
{Value, Req2} = cowboy_http_req:header(Header, Req), | |
case Value of | |
undefined -> next(Req2, State, undefined, OnUndefined); | |
_ -> next(Req2, State, Value, OnDefined) | |
end. | |
%% @private | |
check_content(Req, State, Header, Callback, Default, OnSuccess, OnFailure) -> | |
{Provided0, Req2, State1} = call(Req, State, Callback, []), | |
Provided = Provided0 ++ Default, | |
State2 = State1#state{ | |
variances = case length(Provided) > 1 of | |
true -> [Header, State1#state.variances]; | |
false -> State1#state.variances | |
end | |
}, | |
check_header(Req2, State2, Header, | |
fun | |
(<<>>, Req3, State3) -> | |
next(Req3, State3, hd(Provided), OnSuccess); | |
(<<"*">>, Req3, State3) -> | |
next(Req3, State3, hd(Provided), OnSuccess); | |
(Accepted, Req3, State3) -> | |
case choose(match_header(Header), Provided, parse_header(Header, Accepted)) of | |
undefined -> next(Req3, State3, undefined, OnFailure); | |
Chosen -> next(Req3, State3, Chosen, OnSuccess) | |
end | |
end, | |
fun(Req3, State3) -> | |
next(Req3, State3, hd(Provided), OnSuccess) | |
end | |
). | |
%% @private | |
check_content(Req, State, Header, Callback, Default, OnSuccess) -> | |
check_content(Req, State, Header, Callback, Default, OnSuccess, 406). | |
%% @private | |
call(Req, #state{resource = Resource, resource_state = ResourceState} = State, Callback) -> | |
case erlang:function_exported(Resource, Callback, 2) of | |
true -> | |
{Reply, Req2, ResourceState2} = Resource:Callback(Req, ResourceState), | |
{Reply, Req2, State#state{resource_state = ResourceState2}}; | |
false -> no_call | |
end. | |
%% @private | |
call(Req, State, Callback, Default) -> | |
case call(Req, State, Callback) of | |
no_call -> {Default, Req, State}; | |
Reply -> Reply | |
end. | |
%% @private | |
next(Req, State, _Value, Next) when is_function(Next, 2) -> | |
Next(Req, State); | |
next(Req, State, Value, Next) when is_function(Next, 3) -> | |
Next(Value, Req, State); | |
next(Req, State, _Value, StatusCode) when is_atom(StatusCode) -> | |
reply(Req, State, StatusCode); | |
next(Req, State, _Value, StatusCode) when is_integer(StatusCode) -> | |
reply(Req, State, StatusCode). | |
%% @private | |
%% Service available? | |
b13(Req, State) -> | |
check_resource(Req, State, service_available, true, fun b12/2, 503). | |
%% @private | |
%% Known Method? | |
b12(#http_req{method = Method} = Req, State) -> | |
check_resource(Req, State, known_methods, fun(Methods) -> | |
lists:member(Method, Methods) | |
end, fun b11/2, 501). | |
%% @private | |
%% URI too long? | |
b11(Req, State) -> | |
check_resource(Req, State, uri_too_long, false, fun b10/2, 414). | |
%% @private | |
%% Method allowed? | |
b10(#http_req{method = Method} = Req, State) -> | |
check_resource(Req, State, allowed_methods, | |
fun(Methods) -> lists:member(Method, Methods) end, | |
fun(Methods, Req2, State2) -> | |
Allow = list_to_binary(string:join([atom_to_list(M) || M <- Methods], ", ")), | |
{Req3, State3} = add_response_header({'Allow', Allow}, {Req2, State2}), | |
b9(Req3, State3) | |
end, | |
fun(Methods, Req2, State2) -> | |
Allow = list_to_binary(string:join([atom_to_list(M) || M <- Methods], ", ")), | |
reply(Req2, State2, 405, [{'Allow', Allow}]) | |
end | |
). | |
%% @private | |
%% Content-MD5 present? | |
b9(Req, State) -> | |
check_header(Req, State, 'Content-MD5', fun b9a/3, fun b9b/2). | |
%% @private | |
%% Content-MD5 valid? | |
b9a(_ContentMD5, Req, State) -> | |
b9b(Req, State). | |
%% @private | |
%% Malformed? | |
b9b(Req, State) -> | |
check_resource(Req, State, malformed_request, false, fun b8/2, 400). | |
%% @private | |
%% Authorized? | |
b8(Req, State) -> | |
check_resource(Req, State, is_authorized, true, | |
fun b7/2, | |
fun(AuthHead, Req2, State2) -> | |
reply(Req2, State2, 401, [ | |
{'WWW-Authenticate', AuthHead} | |
]) | |
end | |
). | |
%% @private | |
%% Forbidden? | |
b7(Req, State) -> | |
check_resource(Req, State, forbidden, false, fun b6/2, 403). | |
%% @private | |
%% Okay Content-* Headers? | |
b6(Req, State) -> | |
check_resource(Req, State, valid_content_headers, true, fun b5/2, 501). | |
%% @private | |
%% Known Content-Type? | |
b5(Req, State) -> | |
check_resource(Req, State, known_content_type, true, fun b4/2, 413). | |
%% @private | |
%% Req Entity Too Large? | |
b4(Req, State) -> | |
check_resource(Req, State, valid_entity_length, true, fun b3/2, 413). | |
%% @private | |
%% OPTIONS? | |
b3(#http_req{method = 'OPTIONS'} = Req, State) -> | |
{Headers, Req2, State2} = call(Req, State, options, []), | |
reply(Req2, State2, 200, [Headers | State#state.response_headers]); | |
b3(Req, State) -> | |
c3(Req, State). | |
%% @private | |
%% Accept exists? | |
c3(Req, State) -> | |
check_content(Req, State, 'Accept', content_types_provided, [], fun(ContentType, Req2, State2) -> | |
d4(Req2, State2#state{content_type = ContentType}) | |
end). | |
%% @private | |
%% Accept-Language exists? | |
d4(Req, State) -> | |
% TODO: implement | |
e5(Req, State). | |
%% @private | |
%% Accept-Charset exists? | |
e5(Req, State) -> | |
Default = [ | |
{<<"*">>, fun(Content) -> Content end} | |
], | |
check_content(Req, State, 'Accept-Charset', charsets_provided, Default, fun(ContentCharset, Req2, State2) -> | |
f6(Req2, State2#state{content_charset = ContentCharset}) | |
end). | |
%% @private | |
%% Accept-Encoding exists? | |
f6(Req, State) -> | |
Default = [ | |
{<<"identity">>, fun(Content) -> Content end} | |
], | |
check_content(Req, State, 'Accept-Encoding', encodings_provided, Default, fun(Encoding, Req2, State2) -> | |
g7(Req2, State2#state{content_encoding = Encoding}) | |
end). | |
%% @private | |
%% Resource exists? | |
g7(Req, #state{variances = _Variances} = State) -> | |
check_resource(Req, State, resource_exists, true, fun g8/2, fun h7/2). | |
%% @private | |
%% If-Match exists? | |
g8(Req, State) -> | |
check_header(Req, State, 'If-Match', | |
fun | |
(<<"*">>, Req2, State2) -> h10(Req2, State2); | |
(Value, Req2, State2) -> g11(Value, Req2, State2) | |
end, | |
fun h10/2 | |
). | |
%% @private | |
%% ETag in If-Match? | |
g11(IfMatch, Req, State) -> | |
check_resource(Req, State, generate_etag, fun(Etag) -> | |
not lists:member(Etag, parse_strings(IfMatch)) | |
end, 412, fun h10/2). | |
%% @private | |
%% If-Match exists? | |
h7(Req, State) -> | |
check_header(Req, State, 'If-Match', | |
fun | |
(<<"*">>, Req2, State2) -> reply(Req2, State2, 412); | |
(_Value, Req2, State2) -> i7(Req2, State2) | |
end, | |
fun i7/2 | |
). | |
%% @private | |
%% If-Unmodified-Since exists? | |
h10(Req, State) -> | |
check_header(Req, State, 'If-Unmodified-Since', fun h11/3, fun i12/2). | |
%% @private | |
%% If-Unmodified-Since is valid date? | |
h11(IfUnmodifiedSince, Req, State) -> | |
try parse_header('If-Unmodified-Since', IfUnmodifiedSince) of | |
Date -> h12(Date, Req, State) | |
catch | |
error:badarg -> i12(Req, State) | |
end. | |
%% @private | |
%% Last-Modified =< If-Unmodified-Since? | |
h12(IfUnmodifiedSince, Req, State) -> | |
check_resource(Req, State, last_modified, fun(LastModified) -> | |
LastModified =< IfUnmodifiedSince | |
end, fun i12/2, 412). | |
%% @private | |
%% Moved permanently? (apply PUT to different URI) | |
i4(Req, State) -> | |
check_resource(Req, State, moved_permanently, false, fun p3/2, fun(Uri, Req2, State2) -> | |
reply(Req2, State2, 301, [ | |
{'Location', Uri} | |
]) | |
end). | |
%% @private | |
%% PUT? | |
i7(#http_req{method = 'PUT'} = Req, State) -> | |
i4(Req, State); | |
i7(Req, State) -> | |
k7(Req, State). | |
%% @private | |
%% If-none-match exists? | |
i12(Req, State) -> | |
check_header(Req, State, 'If-None-Match', fun i13/3, fun l13/2). | |
%% @private | |
%% If-None-Match: * exists? | |
i13(<<"*">>, Req, State) -> | |
j18(Req, State); | |
i13(Value, Req, State) -> | |
k13(Value, Req, State). | |
%% @private | |
%% GET or HEAD? | |
j18(#http_req{method = Method} = Req, State) when Method == 'GET'; Method == 'HEAD' -> | |
reply(Req, State, 304); | |
j18(Req, State) -> | |
reply(Req, State, 412). | |
%% @private | |
%% Etag in If-None-Match? | |
k13(IfNoneMatch, Req, State) -> | |
check_resource(Req, State, generate_etag, fun(Etag) -> | |
not lists:member(Etag, parse_strings(IfNoneMatch)) | |
end, fun j18/2, fun l13/2). | |
%% @private | |
%% Previously existed? | |
k7(Req, State) -> | |
check_resource(Req, State, previously_existed, false, fun l7/2, fun k5/2). | |
%% @private | |
%% Moved permanently? | |
k5(Req, State) -> | |
check_resource(Req, State, moved_permanently, false, fun l5/2, fun(Uri, Req2, State2) -> | |
reply(Req2, State2, 301, [ | |
{'Location', Uri} | |
]) | |
end). | |
%% @private | |
%% Moved temporarily? | |
l5(Req, State) -> | |
check_resource(Req, State, moved_temporarily, false, fun m5/2, fun(Uri, Req2, State2) -> | |
reply(Req2, State2, 307, [ | |
{'Location', Uri} | |
]) | |
end). | |
%% @private | |
%% POST? | |
l7(#http_req{method = 'POST'} = Req, State) -> | |
m7(Req, State); | |
l7(Req, State) -> | |
reply(Req, State, 404). | |
%% @private | |
%% If-Modified-Since exists? | |
l13(Req, State) -> | |
check_header(Req, State, 'If-Modified-Since', fun l14/3, fun m16/2). | |
%% @private | |
%% If-Modified-Since is valid date? | |
l14(IfModifiedSince, Req, State) -> | |
try parse_header('If-Modified-Since', IfModifiedSince) of | |
Date -> l15(Date, Req, State) | |
catch | |
error:badarg -> m16(Req, State) | |
end. | |
%% @private | |
%% If-Modified-Since > Now | |
l15(IfModifiedSince, Req, State) -> | |
Now = calendar:universal_time(), | |
case IfModifiedSince > Now of | |
true -> m16(Req, State); | |
false -> l17(IfModifiedSince, Req, State) | |
end. | |
%% @private | |
%% Last-Modified > If-Modified-Since? | |
l17(IfModifiedSince, Req, State) -> | |
check_resource(Req, State, last_modified, fun(LastModified) -> | |
LastModified > IfModifiedSince | |
end, fun m16/2, 304). | |
%% @private | |
%% POST? | |
m5(#http_req{method = 'POST'} = Req, State) -> | |
n5(Req, State); | |
m5(Req, State) -> | |
reply(Req, State, 410). | |
%% @private | |
%% Server allows POST to missing resource? | |
m7(Req, State) -> | |
check_resource(Req, State, allow_missing_post, true, fun n11/2, 404). | |
%% @private | |
%% DELETE? | |
m16(#http_req{method = 'DELETE'} = Req, State) -> | |
m20(Req, State); | |
m16(Req, State) -> | |
n16(Req, State). | |
%% @private | |
%% DELETE enacted immediately? (Also where DELETE is forced.) | |
m20(Req, State) -> | |
check_resource(Req, State, delete_resource, true, fun (Req2, State2) -> | |
check_resource(Req2, State2, delete_completed, true, fun o20/2, 202) | |
end, 500). | |
%% @private | |
%% Server allows POST to missing resource? | |
n5(Req, State) -> | |
check_resource(Req, State, allow_missing_post, true, fun n11/2, 410). | |
%% @private | |
%% process POST | |
n11(Req, State) -> | |
case call(Req, State, post_is_create, false) of | |
{false, Req2, State2} -> | |
case call(Req2, State2, process_post) of | |
no_call -> | |
reply(Req2, State2, 501); | |
{Content, Req3, State3} -> | |
p11(Req3, State3#state{response_body = Content}); | |
{Headers, Content, Req3, State3} -> | |
{Req4, State4} = add_response_headers(Headers, {Req3, State3#state{response_body = Content}}), | |
case lists:keyfind('Location', 1, Headers) of | |
false -> p11(Req4, State4); | |
_ -> reply(Req4, State4, 303) | |
end | |
end; | |
{true, Req2, State2} -> | |
erlang:exit({not_implemented, Req2, State2}) | |
end. | |
%% @private | |
%% POST? | |
n16(#http_req{method = 'POST'} = Req, State) -> | |
n11(Req, State); | |
n16(Req, State) -> | |
o16(Req, State). | |
%% @private | |
%% Conflict? | |
o14(Req, State) -> | |
check_resource(Req, State, is_conflict, false, fun(Req2, State2) -> | |
erlang:exit({not_implemented, Req2, State2}) | |
end, 409). | |
%% @private | |
%% PUT? | |
o16(#http_req{method = 'PUT'} = Req, State) -> | |
o14(Req, State); | |
o16(Req, State) -> | |
o18(Req, State). | |
%% @private | |
%% Multiple representations? | |
%% Also where body generation for GET and HEAD is done. | |
o18(#http_req{method = Method} = Req, State) when Method =:= 'GET'; Method =:= 'HEAD' -> | |
Header = fun({Req0, State0}, Callback, BuildHeader) -> | |
case call(Req0, State0, Callback) of | |
no_call -> {Req0, State0}; | |
{Value, R, S} -> add_response_header(BuildHeader(Value), {R, S}) | |
end | |
end, | |
{Req1, State1} = Header(Header(Header({Req, State}, | |
generate_etag, fun(ETag) -> {<<"ETag">>, build_string(ETag)} end), | |
last_modified, fun(Date) -> {'Last-Modified', build_date(Date)} end), | |
expires, fun(Date) -> {'Expires', build_date(Date)} end), | |
{ContentType, ContentProvider} = State1#state.content_type, | |
{Response, Req2, RState} = ContentProvider(Req1, State1#state.resource_state), | |
State2 = State1#state{resource_state = RState}, | |
{Req3, State3} = case Response of | |
{Headers, Content} when is_list(Headers) -> | |
H = Headers ++ [{'Content-Type', ContentType}], | |
add_response_headers(H, {Req2, State2#state{response_body = Content}}); | |
Content -> | |
H = {'Content-Type', ContentType}, | |
add_response_header(H, {Req2, State2#state{response_body = Content}}) | |
end, | |
o18b(Req3, State3); | |
o18(Req, State) -> | |
o18b(Req, State). | |
%% @private | |
%% Multiple choices? | |
o18b(Req, State) -> | |
check_resource(Req, State, multiple_choices, false, 200, 300). | |
%% @private | |
%% Response includes an entity? | |
o20(Req, #state{response_body = undefined} = State) -> | |
reply(Req, State, 204); | |
o20(Req, State) -> | |
o18(Req, State). | |
%% @private | |
%% Conflict? | |
p3(Req, State) -> | |
check_resource(Req, State, is_conflict, false, fun(Req2, State2) -> | |
erlang:exit({not_implemented, Req2, State2}) | |
end, 409). | |
%% @private | |
%% New resource? | |
p11(Req, State) -> | |
case get_response_header('Location', Req, State) of | |
undefined -> o20(Req, State); | |
{Req2, State2} -> reply(Req2, State2, 201) | |
end. | |
%% @private | |
choose(_, _Provided, []) -> | |
undefined; | |
choose(Matcher, Provided, [Accept | Rest]) -> | |
case match(Matcher, Provided, Accept) of | |
[Choice|_] -> Choice; | |
[] -> choose(Matcher, Provided, Rest) | |
end. | |
%% @private | |
match(_, [], _Accepted) -> []; | |
match(_, Provided, {<<"*/*">>, _}) -> Provided; | |
match(_, Provided, {<<"*">>, _}) -> Provided; | |
match(Matcher, Provided, {AcceptedType, _AcceptedParams}) -> | |
[{Type, Params} || {Type, Params} <- Provided, Matcher(Type, AcceptedType)]. | |
%% @private | |
match_header('Accept') -> fun match_media_type/2; | |
match_header('Accept-Encoding') -> fun match_encoding/2; | |
match_header('Accept-Charset') -> fun match_encoding/2. | |
%% @private | |
match_media_type(_Provided, <<"*">>) -> true; | |
match_media_type(_Provided, <<"*/*">>) -> true; | |
match_media_type(Provided, Provided) -> true; | |
match_media_type(Provided, Accepted) -> | |
[P1 | _P2] = binary:split(Provided, <<"/">>), | |
[A1 | A2] = binary:split(Accepted, <<"/">>), | |
case A2 of | |
[<<"*">>] when A1 == P1 -> true; | |
_ -> false | |
end. | |
%% @private | |
match_encoding(_Provided, <<"*">>) -> true; | |
match_encoding(Provided, Provided) -> true; | |
match_encoding(_Provided, _Accepted) -> false. | |
%% @private | |
parse_header('Accept', Accept) -> | |
prioritize(parse_parametrized(Accept)); | |
parse_header('Accept-Charset', Charsets) -> | |
prioritize(parse_parametrized(Charsets)); | |
parse_header('Accept-Encoding', Encodings) -> | |
prioritize(parse_parametrized(Encodings)) ++ [{<<"identity">>, []}]; | |
parse_header('If-Unmodified-Since', Date) -> | |
parse_date(Date); | |
parse_header('If-Modified-Since', Date) -> | |
parse_date(Date). | |
%% @private | |
prioritize(Choices) -> | |
%% TODO: implement | |
Choices. | |
%% @private | |
parse_date(Date) -> | |
try httpd_util:convert_request_date(binary_to_list(Date)) of | |
bad_date -> erlang:error(badarg, Date); | |
D -> D | |
catch | |
error:_ -> erlang:error(badarg, Date) | |
end. | |
%% @private | |
build_date(Date) -> | |
list_to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Date))). | |
%% @private | |
parse_parametrized(Value) -> | |
[case binary:split(Token, <<";">>) of | |
[Type, Args] -> {strip(Type), parse_kv(Args)}; | |
[Type] -> {strip(Type), []} | |
end || Token <- binary:split(Value, <<",">>, [global, trim])]. | |
%% @private | |
parse_kv(Value) -> | |
parse_kv(binary:split(Value, [<<";">>, <<"=">>], [global, trim]), []). | |
parse_kv([], Acc) -> | |
lists:reverse(Acc); | |
parse_kv([_], Acc) -> | |
lists:reverse(Acc); | |
parse_kv([Key, Value | Rest], Acc) -> | |
case strip(Key) of | |
<<>> -> parse_kv(Rest, Acc); | |
K -> parse_kv(Rest, [{K, strip(Value)} | Acc]) | |
end. | |
%% @private | |
parse_strings(Value) -> | |
parse_strings(Value, []). | |
parse_strings(<<>>, Acc) -> | |
lists:reverse(Acc); | |
parse_strings(<<$", Rest/binary>>, Acc) -> | |
{String, Cont} = parse_string(Rest, <<>>), | |
parse_strings(Cont, [String | Acc]); | |
parse_strings(<<_/utf8, Rest/binary>>, Acc) -> | |
parse_strings(Rest, Acc). | |
parse_string(<<>>, Acc) -> | |
{Acc, <<>>}; | |
parse_string(<<$\\, Char/utf8, Rest/binary>>, Acc) -> | |
parse_string(Rest, <<Acc/binary, Char/utf8>>); | |
parse_string(<<$", Rest/binary>>, Acc) -> | |
{Acc, Rest}; | |
parse_string(<<Char/utf8, Rest/binary>>, Acc) -> | |
parse_string(Rest, <<Acc/binary, Char/utf8>>). | |
%% @private | |
build_string(<<$", _/binary>> = String) -> | |
String; | |
build_string(String) -> | |
build_string(String, <<$">>). | |
build_string(<<>>, Acc) -> | |
<<Acc/binary, $">>; | |
build_string(<<$\\, Char/utf8, Rest/binary>>, Acc) -> | |
build_string(Rest, <<Acc/binary, $\\, Char/utf8>>); | |
build_string(<<$", Rest/binary>>, Acc) -> | |
build_string(Rest, <<Acc/binary, $\\, $">>); | |
build_string(<<Char/utf8, Rest/binary>>, Acc) -> | |
build_string(Rest, <<Acc/binary, Char/utf8>>). | |
%% @private | |
strip(Binary) -> | |
strip_right(strip_left(Binary)). | |
%% @private | |
strip_left(<<C:8, Rest/binary>>) when C == $\s; C == $\t; C == $\n; C == $\r -> | |
strip_left(Rest); | |
strip_left(Stripped) -> | |
Stripped. | |
%% @private | |
strip_right(<<C:8, Rest/binary>>) when C == $\s; C == $\t; C == $\n; C == $\r -> | |
case strip_right(Rest) of | |
<<>> -> <<>>; | |
T -> <<C:8, T/binary>> | |
end; | |
strip_right(<<C:8, Rest/binary>>) -> | |
<<C:8, (strip_right(Rest))/binary>>; | |
strip_right(<<>>) -> | |
<<>>. | |
-ifdef(TEST). | |
-include_lib("eunit/include/eunit.hrl"). | |
parse_header_test_() -> | |
Tests = [ | |
{<<"ISO-8859-1,utf-8;q=0.7,*;q=0.3">>, [ | |
{<<"ISO-8859-1">>,[]}, | |
{<<"utf-8">>,[{<<"q">>,<<"0.7">>}]}, | |
{<<"*">>,[{<<"q">>,<<"0.3">>}]} | |
]} | |
], | |
[{String, fun() -> | |
?assertEqual(Expect, parse_header('Accept-Charset', String)) | |
end} || {String, Expect} <- Tests]. | |
parse_parametrized_test_() -> | |
Tests = [ | |
{<<>>, []}, | |
{<<"text/html">>, [{<<"text/html">>, []}]}, | |
{<<"text/html;q=1.0">>, [{<<"text/html">>, [{<<"q">>, <<"1.0">>}]}]}, | |
{<<"text/html;q=1.0;v=hello">>, [{<<"text/html">>, [{<<"q">>, <<"1.0">>}, {<<"v">>,<<"hello">>}]}]}, | |
{<<"text/html;q=1.0;v=hello,*/*">>, [ | |
{<<"text/html">>, [{<<"q">>, <<"1.0">>}, {<<"v">>,<<"hello">>}]}, | |
{<<"*/*">>, []} | |
]} | |
], | |
[{String, fun() -> | |
?assertEqual(Expect, parse_parametrized(String)) | |
end} || {String, Expect} <- Tests]. | |
parse_kv_test_() -> | |
Tests = [ | |
{<<>>, []}, | |
{<<"q=1.0;v=hello">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]}, | |
{<<"q=1.0; v=hello">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]}, | |
{<<"q = 1.0 ; v = hello">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]}, | |
{<<"q = 1.0 ; v = hello;w=">>, [{<<"q">>,<<"1.0">>}, {<<"v">>,<<"hello">>}]} | |
], | |
[{String, fun() -> | |
?assertEqual(Expect, parse_kv(String)) | |
end} || {String, Expect} <- Tests]. | |
parse_strings_test_() -> | |
Tests = [ | |
{<<>>, []}, | |
{<<"\"\"">>, [<<"">>]}, | |
{<<"\"hello\"">>, [<<"hello">>]}, | |
{<<"\"hello\", \"world\"">>, [<<"hello">>, <<"world">>]}, | |
{<<"\"hello\" wonderful \"world\"">>, [<<"hello">>, <<"world">>]} | |
], | |
[{String, fun() -> | |
?assertEqual(Expect, parse_strings(String)) | |
end} || {String, Expect} <- Tests]. | |
build_string_test_() -> | |
Tests = [ | |
{<<>>, <<"\"\"">>}, | |
{<<"hello">>, <<"\"hello\"">>}, | |
{<<"hello world">>, <<"\"hello world\"">>}, | |
{<<"hello \"world\"">>, <<"\"hello \\\"world\\\"\"">>} | |
], | |
[{String, fun() -> | |
?assertEqual(Expect, build_string(String)) | |
end} || {String, Expect} <- Tests]. | |
strip_test_() -> | |
Tests = [ | |
{<<>>, {<<>>, <<>>, <<>>}}, | |
{<<" a">>, {<<"a">>, <<" a">>, <<"a">>}}, | |
{<<" a ">>, {<<"a ">>, <<" a">>, <<"a">>}}, | |
{<<" aaa bbb ">>, {<<"aaa bbb ">>, <<" aaa bbb">>, <<"aaa bbb">>}} | |
], | |
[{String, fun() -> | |
{Left, Right, Both} = Expect, | |
?assertEqual(Left, strip_left(String)), | |
?assertEqual(Right, strip_right(String)), | |
?assertEqual(Both, strip(String)) | |
end} || {String, Expect} <- Tests]. | |
-endif. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment