From 54a6fdb729cc54a222a28f2449ec00937958398c Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Thu, 30 May 2019 10:35:54 +0300 Subject: [PATCH] Handle XML parsing errors --- apps/epp_proxy/include/epp_proxy.hrl | 15 +- apps/epp_proxy/src/epp_http_client.erl | 38 ++++ .../src/epp_http_client_behaviour.erl | 9 + apps/epp_proxy/src/epp_router.erl | 17 +- apps/epp_proxy/src/epp_tls_worker.erl | 192 ++++++++++++------ apps/epp_proxy/test/epp_router_tests.erl | 6 + config/sys.config | 7 +- config/test.config | 1 + 8 files changed, 217 insertions(+), 68 deletions(-) create mode 100644 apps/epp_proxy/src/epp_http_client.erl create mode 100644 apps/epp_proxy/src/epp_http_client_behaviour.erl diff --git a/apps/epp_proxy/include/epp_proxy.hrl b/apps/epp_proxy/include/epp_proxy.hrl index d887f21..08a8a3d 100644 --- a/apps/epp_proxy/include/epp_proxy.hrl +++ b/apps/epp_proxy/include/epp_proxy.hrl @@ -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{}. diff --git a/apps/epp_proxy/src/epp_http_client.erl b/apps/epp_proxy/src/epp_http_client.erl new file mode 100644 index 0000000..45f0a22 --- /dev/null +++ b/apps/epp_proxy/src/epp_http_client.erl @@ -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]]. diff --git a/apps/epp_proxy/src/epp_http_client_behaviour.erl b/apps/epp_proxy/src/epp_http_client_behaviour.erl new file mode 100644 index 0000000..fc6d5d3 --- /dev/null +++ b/apps/epp_proxy/src/epp_http_client_behaviour.erl @@ -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(). diff --git a/apps/epp_proxy/src/epp_router.erl b/apps/epp_proxy/src/epp_router.erl index 359d197..a604ffa 100644 --- a/apps/epp_proxy/src/epp_router.erl +++ b/apps/epp_proxy/src/epp_router.erl @@ -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. diff --git a/apps/epp_proxy/src/epp_tls_worker.erl b/apps/epp_proxy/src/epp_tls_worker.erl index 94d7668..a717338 100644 --- a/apps/epp_proxy/src/epp_tls_worker.erl +++ b/apps/epp_proxy/src/epp_tls_worker.erl @@ -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. diff --git a/apps/epp_proxy/test/epp_router_tests.erl b/apps/epp_proxy/test/epp_router_tests.erl index 4a96f7d..1b29fe0 100644 --- a/apps/epp_proxy/test/epp_router_tests.erl +++ b/apps/epp_proxy/test/epp_router_tests.erl @@ -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">>)). diff --git a/config/sys.config b/config/sys.config index 358d969..a2c545c 100644 --- a/config/sys.config +++ b/config/sys.config @@ -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]} + ]} + ]} ]. diff --git a/config/test.config b/config/test.config index 5492d79..dae8f0c 100644 --- a/config/test.config +++ b/config/test.config @@ -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"}]}