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, -record(epp_request,
{method, % get | post (atom) {method, % get | post (atom)
url, % "https://example.com/some-url" url, % "https://example.com/some-url"
body, % "" | {multipart [{{<<"raw_frame">>, "Some body"}}]} body, % "" | {multipart [{{<<"raw_frame">>, "Some body"}}]}
cookies, % "" | {multipart [{{<<"raw_frame">>, "Some body"}}]} cookies, % [<<"session=SomeSession; Version=1">>]
headers % [{"User-Agent", <<"EPP proxy">>}, {"Other", <<"Header">>}] 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 {ok, Value} -> Value
end). 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. %% Save yourself some checking beforehand.
is_valid_epp_command(Command) -> is_valid_epp_command(Command) ->
lists:member(Command, ?validCommands). lists:member(Command, ?validCommands).
@ -27,6 +33,10 @@ request_method("hello") ->
get; get;
request_method(<<"hello">>) -> request_method(<<"hello">>) ->
get; get;
request_method("error") ->
get;
request_method(<<"error">>) ->
get;
request_method(_) -> request_method(_) ->
post. post.
@ -53,7 +63,9 @@ url_map(Command) when is_list(Command) ->
"renew" -> string:concat(base_command_url(), Command); "renew" -> string:concat(base_command_url(), Command);
"update" -> string:concat(base_command_url(), Command); "update" -> string:concat(base_command_url(), Command);
% Transfer is both poll and query % 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. % Anything else should fail.
end. end.
@ -63,3 +75,6 @@ base_session_url() ->
base_command_url() -> base_command_url() ->
?baseCommandUrl. ?baseCommandUrl.
base_error_url() ->
?baseErrorUrl.

View file

@ -9,9 +9,17 @@
-export([init/1, handle_cast/2, handle_call/3, start_link/1]). -export([init/1, handle_cast/2, handle_call/3, start_link/1]).
-export([code_change/3]). -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) -> init(Socket) ->
lager:info("Created a worker process"), lager:info("Created a worker process"),
@ -21,63 +29,53 @@ init(Socket) ->
start_link(Socket) -> start_link(Socket) ->
gen_server:start_link(?MODULE, Socket, []). gen_server:start_link(?MODULE, Socket, []).
handle_cast(serve, State = #state{socket=Socket}) -> handle_cast(serve, State = #state{socket=Socket}) ->
%% If certificate is revoked, this will fail right away here. %% If certificate is revoked, this will fail right away here.
%% mod_epp does exactly the same thing. %% mod_epp does exactly the same thing.
{ok, SecureSocket} = ssl:handshake(Socket), {ok, SecureSocket} = ssl:handshake(Socket),
NewState = state_from_socket(SecureSocket, State), NewState = state_from_socket(SecureSocket, State),
{noreply, NewState}; {noreply, NewState};
handle_cast(greeting, State = #state{socket=Socket, common_name=SSL_CLIENT_S_DN_CN, handle_cast(greeting, State = #state{socket=Socket,
client_cert=SSL_CLIENT_CERT,
session_id=SessionId, session_id=SessionId,
peer_ip=PeerIp}) -> headers=Headers}) ->
Request = request("hello", SessionId, "", SSL_CLIENT_S_DN_CN,
SSL_CLIENT_CERT, PeerIp, nomatch),
{_Status, _StatusCode, _Headers, ClientRef} = Request = request_from_map(#{command => "hello",
hackney:request(Request#epp_request.method, Request#epp_request.url, session_id => SessionId,
Request#epp_request.headers, Request#epp_request.body, raw_frame => "",
[{cookie, Request#epp_request.cookies}, insecure]), headers => Headers,
cl_trid => nomatch}),
{ok, Body} = hackney:body(ClientRef), {_Status, Body} = epp_http_client:request(Request),
frame_to_socket(Body, Socket), frame_to_socket(Body, Socket),
gen_server:cast(self(), process_command), gen_server:cast(self(), process_command),
{noreply, State#state{socket=Socket, session_id=SessionId}}; {noreply, State#state{socket=Socket, session_id=SessionId}};
handle_cast(process_command, State = #state{socket=Socket, handle_cast(process_command,
common_name=SSL_CLIENT_S_DN_CN, State = #state{socket=Socket,session_id=SessionId,
client_cert=SSL_CLIENT_CERT, headers=Headers}) ->
session_id=SessionId, RawFrame = frame_from_socket(Socket, State),
peer_ip=PeerIp}) ->
Length = case read_length(Socket) of case parse_frame(RawFrame) of
{ok, Data} -> #valid_frame{command=Command, cl_trid=ClTRID} ->
Data; Request = request_from_map(#{command => Command,
{error, _Details} -> session_id => SessionId,
{stop, normal, State} 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, 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), frame_to_socket(Body, Socket),
%% On logout, close the socket. %% On logout, close the socket.
@ -90,10 +88,11 @@ handle_cast(process_command, State = #state{socket=Socket,
gen_server:cast(self(), process_command), gen_server:cast(self(), process_command),
{noreply, State#state{socket=Socket, session_id=SessionId}} {noreply, State#state{socket=Socket, session_id=SessionId}}
end. end.
handle_call(_E, _From, State) -> {noreply, State}. handle_call(_E, _From, State) -> {noreply, State}.
code_change(_OldVersion, State, _Extra) -> {ok, State}. code_change(_OldVersion, State, _Extra) -> {ok, State}.
%% Private function %% Private functions
write_line(Socket, Line) -> write_line(Socket, Line) ->
ok = ssl:send(Socket, Line). ok = ssl:send(Socket, Line).
@ -118,31 +117,66 @@ read_frame(Socket, FrameLength) ->
end. end.
%% Map request and return values. %% Map request and return values.
%% TODO: Make arguments into a map. request_from_map(#{command := "error", session_id := SessionId,
request(Command, SessionId, RawFrame, CommonName, ClientCert, PeerIp, ClTRID) -> 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), URL = epp_router:route_request(Command),
RequestMethod = epp_router:request_method(Command), RequestMethod = epp_router:request_method(Command),
Cookie = hackney_cookie:setcookie("session", SessionId, []), Cookie = hackney_cookie:setcookie("session", SessionId, []),
case Command of Body = request_body(Command, RawFrame, ClTRID),
"hello" -> Headers=Headers,
Body = ""; Request = #epp_request{url=URL,
_ -> method=RequestMethod,
Body = {multipart, request_body(RawFrame, ClTRID)} body=Body,
end, 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}, Headers = [{"SSL_CLIENT_CERT", ClientCert},
{"SSL_CLIENT_S_DN_CN", CommonName}, {"SSL_CLIENT_S_DN_CN", CommonName},
{"User-Agent", <<"EPP proxy">>}, {"User-Agent", <<"EPP proxy">>},
{"X-Forwarded-for", epp_util:readable_ip(PeerIp)}], {"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}, headers=Headers},
lager:info("Request to be sent: [~p]~n", [Request]), lager:info("Request from map: [~p]~n", [Request]),
Request. Request.
%% Return form data %% Return form data or an empty list.
request_body(RawFrame, nomatch) -> request_body("hello", _, _) ->
[{<<"raw_frame">>, RawFrame}]; "";
request_body(RawFrame, ClTRID) -> request_body(_Command, RawFrame, nomatch) ->
[{<<"raw_frame">>, RawFrame}, {<<"clTRID">>, ClTRID}]. {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. %% Wrap a message in EPP frame, and then send it to socket.
frame_to_socket(Message, Socket) -> frame_to_socket(Message, Socket) ->
@ -151,13 +185,47 @@ frame_to_socket(Message, Socket) ->
write_line(Socket, ByteSize), write_line(Socket, ByteSize),
write_line(Socket, Message). 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. %% Extract state info from socket. Fail if you must.
state_from_socket(Socket, State) -> state_from_socket(Socket, State) ->
{ok, PeerCert} = ssl:peercert(Socket), {ok, PeerCert} = ssl:peercert(Socket),
{ok, {PeerIp, _PeerPort}} = ssl:peername(Socket), {ok, {PeerIp, _PeerPort}} = ssl:peername(Socket),
{SSL_CLIENT_S_DN_CN, SSL_CLIENT_CERT} = {SSL_CLIENT_S_DN_CN, SSL_CLIENT_CERT} =
epp_certs:headers_from_cert(PeerCert), epp_certs:headers_from_cert(PeerCert),
NewState = State#state{socket=Socket, common_name=SSL_CLIENT_S_DN_CN, Headers = [{"SSL_CLIENT_CERT", SSL_CLIENT_CERT},
client_cert=SSL_CLIENT_CERT, peer_ip=PeerIp}, {"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]), lager:info("Established connection with: [~p]~n", [NewState]),
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() -> request_method_test() ->
?assertEqual(get, epp_router:request_method("hello")), ?assertEqual(get, epp_router:request_method("hello")),
?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("create")),
?assertEqual(post, epp_router:request_method(123)). ?assertEqual(post, epp_router:request_method(123)).
@ -60,3 +62,7 @@ update_url_test() ->
transfer_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")),
?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}, {tls_port, 700},
{epp_session_url, "https://registry.test/epp/session/"}, {epp_session_url, "https://registry.test/epp/session/"},
{epp_command_url, "https://registry.test/epp/command/"}, {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"}, {cacertfile_path, "/opt/shared/ca/certs/ca.crt.pem"},
{certfile_path, "/opt/shared/ca/certs/cert.pem"}, {certfile_path, "/opt/shared/ca/certs/cert.pem"},
{keyfile_path, "/opt/shared/ca/certs/key.pem"}, {keyfile_path, "/opt/shared/ca/certs/key.pem"},
@ -11,7 +12,7 @@
{lager, [ {lager, [
{handlers, [ {handlers, [
{lager_console_backend, info}, {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}, {tls_port, 4444},
{epp_session_url, "https://registry.test/epp/session/"}, {epp_session_url, "https://registry.test/epp/session/"},
{epp_command_url, "https://registry.test/epp/command/"}, {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"}, {cacertfile_path, "/opt/shared/ca/certs/ca.crt.pem"},
{certfile_path, "/opt/shared/ca/certs/apache.crt"}, {certfile_path, "/opt/shared/ca/certs/apache.crt"},
{keyfile_path, "/opt/shared/ca/private/apache.key"}]} {keyfile_path, "/opt/shared/ca/private/apache.key"}]}