Current section
Files
Jump to
Current section
Files
src/xqerl_mod_http_client.erl
%% -------------------------------------------------------------------
%%
%% xqerl - XQuery processor
%%
%% Copyright (c) 2018-2020 Zachary N. Dean All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%% @doc Implementation of the "http://expath.org/ns/http-client" namespace.
%% http://expath.org/spec/http-client
-module(xqerl_mod_http_client).
-include("xqerl.hrl").
-define(NS, <<"http://expath.org/ns/http-client">>).
-define(PX, <<"http">>).
-define(ENS, <<"http://expath.org/ns/error">>).
-define(EPX, <<"experr">>).
-define(SNS, <<"http://www.w3.org/2010/xslt-xquery-serialization">>).
-'module-namespace'({?NS, ?PX}).
-namespaces([]).
-variables([]).
-functions([
{
{qname, ?NS, ?PX, <<"send-request">>},
{seqType, 'item', one_or_many},
[],
{'send-request', 4},
3,
[
{seqType, {kindTest, element, {qname, ?NS, ?PX, <<"request">>}, undefined},
zero_or_one},
{seqType, 'xs:string', zero_or_one},
{seqType, 'item', zero_or_many}
]
},
{
{qname, ?NS, ?PX, <<"send-request">>},
{seqType, 'item', one_or_many},
[],
{'send-request', 3},
2,
[
{seqType, {kindTest, element, {qname, ?NS, ?PX, <<"request">>}, undefined},
zero_or_one},
{seqType, 'xs:string', zero_or_one}
]
},
{
{qname, ?NS, ?PX, <<"send-request">>},
{seqType, 'item', one_or_many},
[],
{'send-request', 2},
1,
[
{seqType, {kindTest, element, {qname, ?NS, ?PX, <<"request">>}, undefined}, zero_or_one}
]
}
]).
%% ====================================================================
%% API functions
%% ====================================================================
-export(['send-request'/2, 'send-request'/3, 'send-request'/4]).
-export([
get_content_media_type/1,
parse_body/3,
read_body/1,
parse_custom_response/1
]).
%% http:send-request($request as element(http:request)?,
%% $href as xs:string?,
%% $bodies as item()*) as item()+
'send-request'(Ctx, Request, Href, Bodies) ->
Uri = xqerl_types:string_value(Href),
Req = (parse_request(Request, Bodies))#{href => Uri},
do_req(Ctx, Req).
%% http:send-request($request as element(http:request)?,
%% $href as xs:string?) as item()+
'send-request'(Ctx, Request, []) ->
'send-request'(Ctx, Request);
'send-request'(Ctx, Request, Href) ->
Uri = xqerl_types:string_value(Href),
Req = (parse_request(Request, []))#{href => Uri},
do_req(Ctx, Req).
%% http:send-request($request as element(http:request)?) as item()+
'send-request'(Ctx, Request) ->
Req = parse_request(Request, []),
do_req(Ctx, Req).
%% ====================================================================
%% Internal functions
%% ====================================================================
parse_request(
#{
nk := element,
nn := {?NS, _, <<"request">>},
at := Atts,
ch := Ch
},
Contents
) ->
AttMap = parse_request_attributes(Atts, #{
'status-only' => false,
'send-authorization' => false,
'follow-redirect' => true,
timeout => 10
}),
#{method := Method} = AttMap,
Headers0 = [
parse_header_attributes(A)
|| #{
nn := {?NS, _, <<"header">>},
at := A
} <- Ch
],
{HasCT, Headers} =
case
[
N
|| {N, _} <- Headers0,
string:lowercase(N) == <<"content-type">>
]
of
[] ->
{false, [{<<"content-type">>, <<"text/xml">>} | Headers0]};
_ ->
{true, Headers0}
end,
parse_request_1(Method, Ch, HasCT, Headers, Contents, AttMap).
parse_request_1(Method, Ch, HasCT, Headers, Contents, AttMap) when Method == put; Method == post ->
[Body] = [C || #{nn := {?NS, _, <<"body">>}} = C <- Ch],
SerialOpts = parse_body_attributes(xqldb_mem_nodes:attributes(Body)),
%?dbg("SerialOpts", {SerialOpts, Body}),
Headers1 =
case SerialOpts of
#{'media-type' := MT} when HasCT == false ->
lists:keyreplace(<<"content-type">>, 1, Headers, {<<"content-type">>, MT});
_ ->
Headers
end,
CT = proplists:get_value(<<"content-type">>, Headers1),
case Contents of
[] ->
try
Content = xqldb_mem_nodes:children(Body),
BodyContent = xqerl_serialize:serialize(Content, SerialOpts),
AttMap#{
headers => Headers1,
body => BodyContent
}
catch
_:_ ->
err_bad_request()
end;
% already serialized
_ when is_binary(Contents) ->
AttMap#{
headers => Headers1,
body => Contents
};
% raw binary
_ when CT == <<"application/octet-stream">> ->
AttMap#{
headers => Headers1,
body => xqerl_types:value(Contents)
};
_ ->
BodyContent = xqerl_serialize:serialize(Contents, SerialOpts),
%?dbg("Contents", Contents),
%?dbg("SerialOpts", SerialOpts),
%?dbg("BodyContent", BodyContent),
AttMap#{
headers => Headers1,
body => BodyContent
}
end;
parse_request_1(_, _Ch, _HasCT, Headers, _Contents, AttMap) ->
AttMap#{
headers => Headers,
body => <<>>
}.
parse_request_attributes([#{nn := {<<>>, _, <<"method">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{method => get_method(Val)});
parse_request_attributes([#{nn := {<<>>, _, <<"href">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{href => Val});
parse_request_attributes([#{nn := {<<>>, _, <<"status-only">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{'status-only' => get_boolean(Val)});
parse_request_attributes([#{nn := {<<>>, _, <<"username">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{username => Val});
parse_request_attributes([#{nn := {<<>>, _, <<"password">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{password => Val});
parse_request_attributes([#{nn := {<<>>, _, <<"auth-method">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{'auth-method' => get_auth_method(Val)});
parse_request_attributes([#{nn := {<<>>, _, <<"send-authorization">>}, sv := Val} | T], Acc) ->
% TODO: find out what to do with this
parse_request_attributes(T, Acc#{'send-authorization' => get_boolean(Val)});
parse_request_attributes([#{nn := {<<>>, _, <<"override-media-type">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{'override-media-type' => Val});
parse_request_attributes([#{nn := {<<>>, _, <<"follow-redirect">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{'follow-redirect' => get_boolean(Val)});
parse_request_attributes([#{nn := {<<>>, _, <<"timeout">>}, sv := Val} | T], Acc) ->
parse_request_attributes(T, Acc#{timeout => get_integer(Val)});
parse_request_attributes([_ | _], _) ->
err_bad_request();
parse_request_attributes([], Acc) ->
V = validate_request_attributes(Acc),
validate_request_attributes_1(V).
validate_request_attributes(
#{
username := _,
password := _,
'auth-method' := _
} = Atts
) ->
Atts;
validate_request_attributes(
#{
username := _,
password := _
} = Atts
) ->
Atts#{'auth-method' => <<"Basic">>};
validate_request_attributes(#{username := _}) ->
err_generic();
validate_request_attributes(#{password := _}) ->
err_generic();
validate_request_attributes(#{'auth-method' := _}) ->
err_generic();
validate_request_attributes(Atts) ->
Atts.
validate_request_attributes_1(#{'status-only' := true} = Atts) ->
Atts#{method := head};
validate_request_attributes_1(Atts) ->
Atts.
parse_header_attributes([
#{nn := {<<>>, _, <<"name">>}, sv := Name},
#{nn := {<<>>, _, <<"value">>}, sv := Val}
]) ->
{Name, Val};
parse_header_attributes([
#{nn := {<<>>, _, <<"value">>}, sv := Val},
#{nn := {<<>>, _, <<"name">>}, sv := Name}
]) ->
{Name, Val};
parse_header_attributes(_) ->
err_bad_request().
% 1:1 serialization
parse_body_attributes(Atts) ->
List = [
{
#qname{
namespace = ?SNS,
prefix = <<>>,
local_name = Nm
},
Sv
}
|| #{nn := {_, _, Nm}, sv := Sv} <- Atts, Nm =/= <<"src">>
],
xqerl_options:serialization_option_map(List, []).
get_integer(Int) ->
try
binary_to_integer(Int)
catch
_:_ ->
err_bad_request()
end.
get_auth_method(Method) ->
case string:lowercase(Method) of
% doesn`t really work
<<"digest">> -> digest;
<<"basic">> -> basic;
_ -> err_bad_request()
end.
get_boolean(<<"true">>) -> true;
get_boolean(<<"yes">>) -> true;
get_boolean(<<"false">>) -> false;
get_boolean(<<"no">>) -> false;
get_boolean(_) -> err_bad_request().
get_method(Method) ->
case string:lowercase(Method) of
<<"get">> ->
get;
<<"post">> ->
post;
<<"head">> ->
head;
<<"options">> ->
options;
<<"put">> ->
put;
<<"delete">> ->
delete;
% no TRACE CONNECT,
_ ->
% HEAD is used when no body requested
err_bad_request()
end.
err_generic() ->
E = #xqError{
description =
"An HTTP error occurred.",
name = #qname{
namespace = ?ENS,
prefix = ?EPX,
local_name = <<"HC001">>
}
},
throw(E).
err_parse() ->
E = #xqError{
description =
"Error parsing the entity content as XML or HTML.",
name = #qname{
namespace = ?ENS,
prefix = ?EPX,
local_name = <<"HC002">>
}
},
throw(E).
%% @TODO: implement multipart
%% ```err_bad_multipart() ->
%% E = #xqError{description =
%% "With a multipart response, the override-media-type must be"
%% " either a multipart media type or application/octet-stream.",
%% name = #qname{namespace = ?ENS,prefix = ?EPX,
%% local_name = <<"HC003">>}
%% },
%% throw(E).
%%'''
%% @TODO: implement src attribute in body
%% ```err_bad_source() ->
%% E = #xqError{description =
%% "The src attribute on the body element is mutually exclusive"
%% " with all other attribute (except the media-type).",
%% name = #qname{namespace = ?ENS,prefix = ?EPX,
%% local_name = <<"HC004">>}
%% },
%% throw(E).'''
err_bad_request() ->
E = #xqError{
description = "The request element is not valid.",
name = #qname{
namespace = ?ENS,
prefix = ?EPX,
local_name = <<"HC005">>
}
},
throw(E).
err_timeout() ->
E = #xqError{
description = "A timeout occurred waiting for the response.",
name = #qname{
namespace = ?ENS,
prefix = ?EPX,
local_name = <<"HC006">>
}
},
throw(E).
do_req(
Ctx,
#{
href := URL,
method := Method,
headers := Headers,
body := Serial,
timeout := TO
} = Opts
) ->
TimeOut = [{recv_timeout, TO * 1000}],
Auth =
case Opts of
#{
username := Username,
password := Password,
'auth-method' := _
} ->
[{basic_auth, {Username, Password}} | TimeOut];
_ ->
TimeOut
end,
Redirect =
case maps:get('follow-redirect', Opts, true) of
true ->
% spec says max one hop
[{follow_redirect, true}, {max_redirect, 1} | Auth];
false ->
Auth
end,
case hackney:request(Method, URL, Headers, Serial, Redirect) of
{error, timeout} ->
err_timeout();
{error, connect_timeout} ->
err_timeout();
{error, _} ->
err_generic();
{ok, StatusCode, RespHeaders, ClientRef} ->
case hackney:body(ClientRef) of
{ok, Body1} ->
resp_element(Ctx, StatusCode, RespHeaders, Body1, Opts);
_ ->
err_generic()
end;
% head
{ok, StatusCode, RespHeaders} ->
resp_element(Ctx, StatusCode, RespHeaders, head, Opts)
end.
resp_element(Ctx, StatusCode, Headers0, Body, #{href := URL} = Opts) ->
Headers = normalize_headers(Headers0),
HdElems = resp_headers_to_elements(Headers),
ContTyp = proplists:get_value(<<"content-type">>, Headers, <<"text/xml">>),
Encoding = proplists:get_value(<<"content-encoding">>, Headers, plain),
[ContTyp1 | _] = string:split(ContTyp, ";"),
ContTyp2 = maps:get('override-media-type', Opts, ContTyp1),
Ref = make_ref(),
ContElem =
element_node(
{?NS, ?PX, <<"body">>},
[attribute(<<"media-type">>, ContTyp2, Ref)],
[],
Ref
),
StatAtts = [
attribute(<<"message">>, code_text(StatusCode), Ref),
attribute(<<"status">>, integer_to_binary(StatusCode), Ref)
],
Elem =
element_node(
{?NS, ?PX, <<"response">>},
StatAtts,
HdElems ++ [ContElem],
Ref
),
case Body of
% methods that have no body
head ->
xqerl_node:contruct(Ctx, Elem);
% zipped
_ when Encoding == <<"gzip">> ->
Body1 = zlib:gunzip(Body),
ParsedBody = parse_body(ContTyp2, Body1, URL),
[xqerl_node:contruct(Ctx, Elem), ParsedBody];
_ ->
ParsedBody = parse_body(ContTyp2, Body, URL),
[xqerl_node:contruct(Ctx, Elem), ParsedBody]
end.
resp_headers_to_elements(Headers) ->
[
begin
Ref = make_ref(),
element_node(
{?NS, ?PX, <<"header">>},
[
attribute(<<"value">>, Value, Ref),
attribute(<<"name">>, Name, Ref)
],
[],
Ref
)
end
|| {Name, Value} <- Headers
].
normalize_headers(Headers) ->
[{string:lowercase(K), V} || {K, V} <- Headers].
attribute(LocalName, Value, Ref) ->
#{
nk => attribute,
id => {Ref, -1},
nn => {<<>>, <<>>, LocalName},
sv => Value,
tn => 'xs:untypedAtomic'
}.
element_node(Name, Atts, Expr, Ref) ->
#{
nk => element,
id => {Ref, -1},
nn => Name,
at => Atts,
ch => Expr,
tn => 'xs:untyped',
ns => #{
<<>> => <<>>,
?PX => ?NS
}
}.
% returns StatusCode, Headers
parse_custom_response(#{
nk := element,
nn := {?NS, _, <<"response">>},
at := Atts,
ch := Headers
}) ->
Status =
case
[
binary_to_integer(StatusStr)
|| #{
nn := {<<>>, _, <<"status">>},
sv := StatusStr
} <- Atts
]
of
[] ->
200;
[V] ->
V
end,
NameVals = [
parse_header_attributes(A)
|| #{
nn := {?NS, _, <<"header">>},
at := A
} <- Headers
],
LowNameVals = [{string:lowercase(Name), Val} || {Name, Val} <- NameVals],
{Status, maps:from_list(LowNameVals)}.
read_body(Req) ->
case cowboy_req:has_body(Req) of
true ->
read_body(Req, <<>>);
false ->
<<>>
end.
read_body(Req, Acc) ->
case cowboy_req:read_body(Req) of
{more, Bin, Req1} ->
read_body(Req1, <<Acc/binary, Bin/binary>>);
{ok, Bin, _} ->
<<Acc/binary, Bin/binary>>
end.
parse_body(MediaTyp, Body, URL) ->
try
case get_content_media_type(MediaTyp) of
text ->
Body;
binary ->
#xqAtomicValue{type = 'xs:base64Binary', value = Body};
_ when Body == <<>> ->
[];
json ->
xqerl_json:string(Body, []);
xml ->
xqldb_mem_nodes:parse_binary(Body, {<<>>, URL});
html ->
xqldb_mem_nodes:parse_binary_html(Body, URL);
csv ->
xqerl_mod_csv:parse(#{}, Body)
end
catch
_:Err:Stack ->
?dbg("Err", Err),
?dbg("Stack", Stack),
?dbg("MediaTyp", MediaTyp),
?dbg("Body", Body),
err_parse()
end.
get_content_media_type(<<"text/xml", _/binary>>) ->
xml;
get_content_media_type(<<"text/html">>) ->
html;
get_content_media_type(<<"text/csv">>) ->
csv;
get_content_media_type(<<"text/", _/binary>>) ->
text;
get_content_media_type(<<"application/octet-stream">>) ->
binary;
get_content_media_type(<<"application/xml">>) ->
xml;
get_content_media_type(<<"application/json">>) ->
json;
get_content_media_type(<<"application/xhtml+xml">>) ->
xhtml;
get_content_media_type(Other) ->
Sz = byte_size(Other),
Tl = Sz - 4,
case Other of
<<_:Tl/binary, "+xml">> when Tl > 0 ->
xml;
_ ->
binary
end.
code_text(200) -> <<"OK">>;
code_text(100) -> <<"Continue">>;
code_text(101) -> <<"Switching Protocols">>;
code_text(102) -> <<"Processing">>;
code_text(103) -> <<"Early Hints">>;
code_text(201) -> <<"Created">>;
code_text(202) -> <<"Accepted">>;
code_text(203) -> <<"Non-Authoritative Information">>;
code_text(204) -> <<"No Content">>;
code_text(205) -> <<"Reset Content">>;
code_text(206) -> <<"Partial Content">>;
code_text(207) -> <<"Multi-Status">>;
code_text(208) -> <<"Already Reported">>;
code_text(226) -> <<"IM Used">>;
code_text(300) -> <<"Multiple Choices">>;
code_text(301) -> <<"Moved Permanently">>;
code_text(302) -> <<"Found">>;
code_text(303) -> <<"See Other">>;
code_text(304) -> <<"Not Modified">>;
code_text(305) -> <<"Use Proxy">>;
code_text(306) -> <<"Switch Proxy">>;
code_text(307) -> <<"Temporary Redirect">>;
code_text(308) -> <<"Permanent Redirect">>;
code_text(400) -> <<"Bad Request">>;
code_text(401) -> <<"Unauthorized">>;
code_text(402) -> <<"Payment Required">>;
code_text(403) -> <<"Forbidden">>;
code_text(404) -> <<"Not Found">>;
code_text(405) -> <<"Method Not Allowed">>;
code_text(406) -> <<"Not Acceptable">>;
code_text(407) -> <<"Proxy Authentication Required">>;
code_text(408) -> <<"Request Timeout">>;
code_text(409) -> <<"Conflict">>;
code_text(410) -> <<"Gone">>;
code_text(411) -> <<"Length Required">>;
code_text(412) -> <<"Precondition Failed">>;
code_text(413) -> <<"Payload Too Large">>;
code_text(414) -> <<"URI Too Long">>;
code_text(415) -> <<"Unsupported Media Type">>;
code_text(416) -> <<"Range Not Satisfiable">>;
code_text(417) -> <<"Expectation Failed">>;
code_text(418) -> <<"I'm a teapot">>;
code_text(421) -> <<"Misdirected Request">>;
code_text(422) -> <<"Unprocessable Entity">>;
code_text(423) -> <<"Locked">>;
code_text(424) -> <<"Failed Dependency">>;
code_text(426) -> <<"Upgrade Required">>;
code_text(428) -> <<"Precondition Required">>;
code_text(429) -> <<"Too Many Requests">>;
code_text(431) -> <<"Request Header Fields Too Large">>;
code_text(451) -> <<"Unavailable For Legal Reasons">>;
code_text(500) -> <<"Internal Server Error">>;
code_text(501) -> <<"Not Implemented">>;
code_text(502) -> <<"Bad Gateway">>;
code_text(503) -> <<"Service Unavailable">>;
code_text(504) -> <<"Gateway Timeout">>;
code_text(505) -> <<"HTTP Version Not Supported">>;
code_text(506) -> <<"Variant Also Negotiates">>;
code_text(507) -> <<"Insufficient Storage">>;
code_text(508) -> <<"Loop Detected">>;
code_text(510) -> <<"Not Extended">>;
code_text(511) -> <<"Network Authentication Required">>;
code_text(_) -> <<"D'oh">>.