Handle XML parsing errors

This commit is contained in:
Maciej Szlosarczyk 2019-05-30 10:35:54 +03:00
parent 73384531c8
commit 54a6fdb729
No known key found for this signature in database
GPG key ID: 41D62D42D3B0D765
8 changed files with 217 additions and 68 deletions

View file

@ -1,8 +1,19 @@
%% This record is used by both epp_tcp_worker and epp_tls_worker.
%% These records are used by both epp_tcp_worker and epp_tls_worker.
-record(epp_request,
{method, % get | post (atom)
url, % "https://example.com/some-url"
body, % "" | {multipart [{{<<"raw_frame">>, "Some body"}}]}
cookies, % "" | {multipart [{{<<"raw_frame">>, "Some body"}}]}
cookies, % [<<"session=SomeSession; Version=1">>]
headers % [{"User-Agent", <<"EPP proxy">>}, {"Other", <<"Header">>}]
}).
-record(epp_error_request,
{method, % get
url, % "https://example.com/some-url"
query_params, % {[{<<"msg">>, <<"Some">>}, {<<"code">>, <<"2001">>}]}
cookies, % [<<"session=SomeSession; Version=1">>]
headers % [{"User-Agent", <<"EPP proxy">>}, {"Other", <<"Header">>}]
}).
-type epp_request() :: #epp_request{}.
-type epp_error_request() :: #epp_error_request{}.

View file

@ -0,0 +1,38 @@
-module(epp_http_client).
-include("epp_proxy.hrl").
-behaviour(epp_http_client_behaviour).
-export([request/1, error_request/1]).
%% Callback API
request(#epp_request{} = Request) ->
HackneyArgs = handle_args(Request),
{Status, _StatusCode, _Headers, ClientRef} =
apply(hackney, request, HackneyArgs),
{ok, Body} = hackney:body(ClientRef),
{Status, Body}.
error_request(#epp_error_request{} = Request) ->
HackneyArgs = handle_error_args(Request),
{Status, _StatusCode, _Headers, ClientRef} =
apply(hackney, request, HackneyArgs),
{ok, Body} = hackney:body(ClientRef),
{Status, Body}.
%% Private API
-spec handle_args(epp_request()) -> list().
handle_args(#epp_request{method=get, url=URL, headers=Headers, body="",
cookies=Cookies}) ->
[get, URL, Headers, "", [{cookie, Cookies}, insecure]];
handle_args(#epp_request{method=post, url=URL, headers=Headers, body=Body,
cookies=Cookies}) ->
[post, URL, Headers, Body, [{cookie, Cookies}, insecure]].
-spec handle_error_args(epp_error_request()) -> list().
handle_error_args(#epp_error_request{method=get, url=URL, headers=Headers,
query_params=Params, cookies=Cookies}) ->
QueryString = hackney_url:qs(Params),
CompleteURL = [URL, <<"?">>, QueryString],
[get, CompleteURL, Headers, "", [{cookie, Cookies}, insecure]].

View file

@ -0,0 +1,9 @@
-module(epp_http_client_behaviour).
-include("epp_proxy.hrl").
-type http_response() :: {integer(), binary()}.
%% Abstract module for http client behaviour. It should call EPP HTTP server
%% and return a response back to the caller.
-callback request(epp_request()) -> http_response().

View file

@ -18,6 +18,12 @@
{ok, Value} -> Value
end).
-define(baseErrorUrl,
case application:get_env(epp_proxy, epp_error_url) of
undefined -> "https://registry.test/epp/error/";
{ok, Value} -> Value
end).
%% Save yourself some checking beforehand.
is_valid_epp_command(Command) ->
lists:member(Command, ?validCommands).
@ -27,6 +33,10 @@ request_method("hello") ->
get;
request_method(<<"hello">>) ->
get;
request_method("error") ->
get;
request_method(<<"error">>) ->
get;
request_method(_) ->
post.
@ -53,7 +63,9 @@ url_map(Command) when is_list(Command) ->
"renew" -> string:concat(base_command_url(), Command);
"update" -> string:concat(base_command_url(), Command);
% Transfer is both poll and query
"transfer" -> string:concat(base_command_url(), Command)
"transfer" -> string:concat(base_command_url(), Command);
% Error route
"error" -> base_error_url()
% Anything else should fail.
end.
@ -63,3 +75,6 @@ base_session_url() ->
base_command_url() ->
?baseCommandUrl.
base_error_url() ->
?baseErrorUrl.

View file

@ -9,9 +9,17 @@
-export([init/1, handle_cast/2, handle_call/3, start_link/1]).
-export([code_change/3]).
-export([request/7]).
-export([request_from_map/1]).
-record(state,{socket, session_id, common_name, client_cert, peer_ip}).
-record(valid_frame, {command,
cl_trid,
raw_frame}).
-record(invalid_frame, {code,
cl_trid,
message}).
-record(state, {socket,
session_id,
headers }).
init(Socket) ->
lager:info("Created a worker process"),
@ -21,63 +29,53 @@ init(Socket) ->
start_link(Socket) ->
gen_server:start_link(?MODULE, Socket, []).
handle_cast(serve, State = #state{socket=Socket}) ->
%% If certificate is revoked, this will fail right away here.
%% mod_epp does exactly the same thing.
{ok, SecureSocket} = ssl:handshake(Socket),
NewState = state_from_socket(SecureSocket, State),
{noreply, NewState};
handle_cast(greeting, State = #state{socket=Socket, common_name=SSL_CLIENT_S_DN_CN,
client_cert=SSL_CLIENT_CERT,
handle_cast(greeting, State = #state{socket=Socket,
session_id=SessionId,
peer_ip=PeerIp}) ->
Request = request("hello", SessionId, "", SSL_CLIENT_S_DN_CN,
SSL_CLIENT_CERT, PeerIp, nomatch),
headers=Headers}) ->
{_Status, _StatusCode, _Headers, ClientRef} =
hackney:request(Request#epp_request.method, Request#epp_request.url,
Request#epp_request.headers, Request#epp_request.body,
[{cookie, Request#epp_request.cookies}, insecure]),
Request = request_from_map(#{command => "hello",
session_id => SessionId,
raw_frame => "",
headers => Headers,
cl_trid => nomatch}),
{ok, Body} = hackney:body(ClientRef),
{_Status, Body} = epp_http_client:request(Request),
frame_to_socket(Body, Socket),
gen_server:cast(self(), process_command),
{noreply, State#state{socket=Socket, session_id=SessionId}};
handle_cast(process_command, State = #state{socket=Socket,
common_name=SSL_CLIENT_S_DN_CN,
client_cert=SSL_CLIENT_CERT,
session_id=SessionId,
peer_ip=PeerIp}) ->
Length = case read_length(Socket) of
{ok, Data} ->
Data;
{error, _Details} ->
{stop, normal, State}
handle_cast(process_command,
State = #state{socket=Socket,session_id=SessionId,
headers=Headers}) ->
RawFrame = frame_from_socket(Socket, State),
case parse_frame(RawFrame) of
#valid_frame{command=Command, cl_trid=ClTRID} ->
Request = request_from_map(#{command => Command,
session_id => SessionId,
raw_frame => RawFrame,
headers => Headers,
cl_trid => ClTRID}),
{_Status, Body} = epp_http_client:request(Request);
#invalid_frame{message=Message, code=Code, cl_trid=ClTRID} ->
Command = "error",
Request = request_from_map(#{command => Command,
session_id => SessionId,
headers => Headers,
code => Code,
message => Message,
cl_trid => ClTRID}),
{_Status, Body} = epp_http_client:error_request(Request)
end,
Frame = case read_frame(Socket, Length) of
{ok, FrameData} ->
io:format("~p~n", [FrameData]),
FrameData;
{error, _FrameDetails} ->
{stop, normal, State}
end,
{ok, XMLRecord} = epp_xml:parse(Frame),
ClTRID= epp_xml:find_cltrid(Frame),
Command = epp_xml:get_command(XMLRecord),
Request = request(Command, SessionId, Frame, SSL_CLIENT_S_DN_CN,
SSL_CLIENT_CERT, PeerIp, ClTRID),
{_Status, _StatusCode, _Headers, ClientRef} =
hackney:request(Request#epp_request.method, Request#epp_request.url,
Request#epp_request.headers, Request#epp_request.body,
[{cookie, Request#epp_request.cookies}, insecure]),
{ok, Body} = hackney:body(ClientRef),
frame_to_socket(Body, Socket),
%% On logout, close the socket.
@ -90,10 +88,11 @@ handle_cast(process_command, State = #state{socket=Socket,
gen_server:cast(self(), process_command),
{noreply, State#state{socket=Socket, session_id=SessionId}}
end.
handle_call(_E, _From, State) -> {noreply, State}.
code_change(_OldVersion, State, _Extra) -> {ok, State}.
%% Private function
%% Private functions
write_line(Socket, Line) ->
ok = ssl:send(Socket, Line).
@ -118,31 +117,66 @@ read_frame(Socket, FrameLength) ->
end.
%% Map request and return values.
%% TODO: Make arguments into a map.
request(Command, SessionId, RawFrame, CommonName, ClientCert, PeerIp, ClTRID) ->
request_from_map(#{command := "error", session_id := SessionId,
code := Code, message := Message, headers:=Headers,
cl_trid := ClTRID}) ->
URL = epp_router:route_request("error"),
RequestMethod = epp_router:request_method("error"),
Cookie = hackney_cookie:setcookie("session", SessionId, []),
QueryParams = query_params(Code, Message, ClTRID),
Headers=Headers,
Request = #epp_error_request{url=URL,
method=RequestMethod,
query_params=QueryParams,
cookies=[Cookie],
headers=Headers},
lager:info("Error Request from map: [~p]~n", [Request]),
Request;
request_from_map(#{command := Command, session_id := SessionId,
raw_frame := RawFrame, headers:=Headers, cl_trid := ClTRID}) ->
URL = epp_router:route_request(Command),
RequestMethod = epp_router:request_method(Command),
Cookie = hackney_cookie:setcookie("session", SessionId, []),
case Command of
"hello" ->
Body = "";
_ ->
Body = {multipart, request_body(RawFrame, ClTRID)}
end,
Body = request_body(Command, RawFrame, ClTRID),
Headers=Headers,
Request = #epp_request{url=URL,
method=RequestMethod,
body=Body,
cookies=[Cookie],
headers=Headers},
lager:info("Request from map: [~p]~n", [Request]),
Request;
request_from_map(#{command := Command, session_id := SessionId,
raw_frame := RawFrame, common_name := CommonName,
client_cert := ClientCert, peer_ip := PeerIp, cl_trid := ClTRID}) ->
URL = epp_router:route_request(Command),
RequestMethod = epp_router:request_method(Command),
Cookie = hackney_cookie:setcookie("session", SessionId, []),
Body = request_body(Command, RawFrame, ClTRID),
Headers = [{"SSL_CLIENT_CERT", ClientCert},
{"SSL_CLIENT_S_DN_CN", CommonName},
{"User-Agent", <<"EPP proxy">>},
{"X-Forwarded-for", epp_util:readable_ip(PeerIp)}],
Request = #epp_request{url=URL, method=RequestMethod, body=Body, cookies=[Cookie],
Request = #epp_request{url=URL,
method=RequestMethod,
body=Body,
cookies=[Cookie],
headers=Headers},
lager:info("Request to be sent: [~p]~n", [Request]),
lager:info("Request from map: [~p]~n", [Request]),
Request.
%% Return form data
request_body(RawFrame, nomatch) ->
[{<<"raw_frame">>, RawFrame}];
request_body(RawFrame, ClTRID) ->
[{<<"raw_frame">>, RawFrame}, {<<"clTRID">>, ClTRID}].
%% Return form data or an empty list.
request_body("hello", _, _) ->
"";
request_body(_Command, RawFrame, nomatch) ->
{multipart, [{<<"raw_frame">>, RawFrame}]};
request_body(_Command, RawFrame, ClTRID) ->
{multipart, [{<<"raw_frame">>, RawFrame}, {<<"clTRID">>, ClTRID}]}.
query_params(Code, Message, nomatch) ->
[{<<"code">>, Code}, {<<"msg">>, Message}];
query_params(Code, Message, ClTRID) ->
[{<<"code">>, Code}, {<<"msg">>, Message}, {<<"clTRID">>, ClTRID}].
%% Wrap a message in EPP frame, and then send it to socket.
frame_to_socket(Message, Socket) ->
@ -151,13 +185,47 @@ frame_to_socket(Message, Socket) ->
write_line(Socket, ByteSize),
write_line(Socket, Message).
%% First, listen for 4 bytes, then listen until the declared length.
%% Return the frame binary at the very end.
frame_from_socket(Socket, State) ->
Length = case read_length(Socket) of
{ok, Data} ->
Data;
{error, _Details} ->
{stop, normal, State}
end,
Frame = case read_frame(Socket, Length) of
{ok, FrameData} ->
io:format("~p~n", [FrameData]),
FrameData;
{error, _FrameDetails} ->
{stop, normal, State}
end,
Frame.
%% Extract state info from socket. Fail if you must.
state_from_socket(Socket, State) ->
{ok, PeerCert} = ssl:peercert(Socket),
{ok, {PeerIp, _PeerPort}} = ssl:peername(Socket),
{SSL_CLIENT_S_DN_CN, SSL_CLIENT_CERT} =
epp_certs:headers_from_cert(PeerCert),
NewState = State#state{socket=Socket, common_name=SSL_CLIENT_S_DN_CN,
client_cert=SSL_CLIENT_CERT, peer_ip=PeerIp},
Headers = [{"SSL_CLIENT_CERT", SSL_CLIENT_CERT},
{"SSL_CLIENT_S_DN_CN", SSL_CLIENT_S_DN_CN},
{"User-Agent", <<"EPP proxy">>},
{"X-Forwarded-for", epp_util:readable_ip(PeerIp)}],
NewState = State#state{socket=Socket, headers=Headers},
lager:info("Established connection with: [~p]~n", [NewState]),
NewState.
%% Get status, XML record, command and clTRID if defined
parse_frame(Frame) ->
ClTRID = epp_xml:find_cltrid(Frame),
case epp_xml:parse(Frame) of
{ok, XMLRecord} ->
Command = epp_xml:get_command(XMLRecord),
#valid_frame{command=Command, cl_trid=ClTRID, raw_frame=Frame};
{error, _} ->
ErrorMessage = <<"Command syntax error.">>,
#invalid_frame{code=2001, message=ErrorMessage, cl_trid=ClTRID}
end.

View file

@ -13,6 +13,8 @@ is_valid_epp_command_test() ->
request_method_test() ->
?assertEqual(get, epp_router:request_method("hello")),
?assertEqual(get, epp_router:request_method(<<"hello">>)),
?assertEqual(get, epp_router:request_method("error")),
?assertEqual(get, epp_router:request_method(<<"error">>)),
?assertEqual(post, epp_router:request_method("create")),
?assertEqual(post, epp_router:request_method(123)).
@ -60,3 +62,7 @@ update_url_test() ->
transfer_url_test() ->
?assertEqual("https://registry.test/epp/command/transfer", epp_router:route_request("transfer")),
?assertEqual("https://registry.test/epp/command/transfer", epp_router:route_request(<<"transfer">>)).
error_url_test() ->
?assertEqual("https://registry.test/epp/error/", epp_router:route_request("error")),
?assertEqual("https://registry.test/epp/error/", epp_router:route_request(<<"error">>)).

View file

@ -4,6 +4,7 @@
{tls_port, 700},
{epp_session_url, "https://registry.test/epp/session/"},
{epp_command_url, "https://registry.test/epp/command/"},
{epp_error_url, "https://registry.test/epp/error/"},
{cacertfile_path, "/opt/shared/ca/certs/ca.crt.pem"},
{certfile_path, "/opt/shared/ca/certs/cert.pem"},
{keyfile_path, "/opt/shared/ca/certs/key.pem"},
@ -11,7 +12,7 @@
{lager, [
{handlers, [
{lager_console_backend, info},
{lager_syslog_backend, ["epp_proxy", local1, info]},
{lager_syslog_backend, ["epp_proxy", local1, info]}
]}
]}
}
].

View file

@ -4,6 +4,7 @@
{tls_port, 4444},
{epp_session_url, "https://registry.test/epp/session/"},
{epp_command_url, "https://registry.test/epp/command/"},
{epp_error_url, "https://registry.test/epp/error/"},
{cacertfile_path, "/opt/shared/ca/certs/ca.crt.pem"},
{certfile_path, "/opt/shared/ca/certs/apache.crt"},
{keyfile_path, "/opt/shared/ca/private/apache.key"}]}