diff options
-rw-r--r-- | Makefile | 9 | ||||
-rw-r--r-- | README | 34 | ||||
-rw-r--r-- | ereproxy.app | 8 | ||||
-rw-r--r-- | ereproxy.erl | 28 | ||||
-rw-r--r-- | ereproxy_config.erl | 31 | ||||
-rw-r--r-- | ereproxy_config.hrl | 6 | ||||
-rw-r--r-- | ereproxy_log.erl | 24 | ||||
-rw-r--r-- | ereproxy_server.erl | 131 | ||||
-rw-r--r-- | example/cert.pem | 15 | ||||
-rw-r--r-- | example/key.pem | 15 |
10 files changed, 301 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..51e3427 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +MODULES=ereproxy_config ereproxy ereproxy_log ereproxy_server + +all: $(addsuffix .beam, $(MODULES)) + +clean: + -rm *{~,.beam} + +%.beam: %.erl + erlc $< @@ -0,0 +1,34 @@ +ereproxy - Erlang REverse http Proxy +==================================== + +1. Features + + - simple + - listen on multiple ports + - frontend speaks HTTP and HTTPS (backend always uses HTTP) + - hot update of code & config possible + - (obscure) custom log format + + +2. Usage + + 1. edit ereproxy_config.erl + 2. [optional] obtain an SSL key/cert if HTTPS support is intended + 1. $ openssl genrsa -out key.pem 1024 + 2. $ openssl req -new -key key.pem -out cert.csr + 3. $ openssl x509 -req -days 666 -in cert.csr -signkey key.pem -out cert.pem + 3. $ make + 4. $ erl -s ereproxy + + +3. Redirection strategy + + Ereproxy parses the _first_ HTTP header of every connection until a + Host line or the end of the header is found. The connection is + routed according to this host. No further headers or even + conformance to the HTTP protocoll is checked. + + +4. author/contact + + Jan Huwald <jh@sotun.de> diff --git a/ereproxy.app b/ereproxy.app new file mode 100644 index 0000000..eee2f52 --- /dev/null +++ b/ereproxy.app @@ -0,0 +1,8 @@ +{application, ereproxy, + [{description, "HTTP reverse proxy"}, + {vsn, "0"}, + {modules, [ereproxy_con, ereproxy_logger]}, + {registered, [ereproxy_logger]}, + {included_applications, [crypto, public_key, ssl]}, + {mod, {ereproxy, ereproxy_config}} + ]}. diff --git a/ereproxy.erl b/ereproxy.erl new file mode 100644 index 0000000..487aeda --- /dev/null +++ b/ereproxy.erl @@ -0,0 +1,28 @@ +-module(ereproxy). +-behaviour(application). +-behaviour(supervisor). +-export([start/0, start/2, stop/1, init/1]). + +start() -> + application:start(?MODULE, permanent). + +%%% application callbacks + +start(_Type, CfgMod) -> + lists:map(fun(N) -> ok = application:start(N) end, + [crypto, public_key, ssl]), + supervisor:start_link(?MODULE, CfgMod). + +stop(_State) -> + ok. + +%%% supervisor callbacks + +init(CfgMod) -> + % return child spec + {ok, {{one_for_one, 10, 3600}, + [{Mod, + {Mod, start_link, [CfgMod]}, + permanent, brutal_kill, worker, + [Mod]} + || Mod <- [ereproxy_server, ereproxy_log]]}}. diff --git a/ereproxy_config.erl b/ereproxy_config.erl new file mode 100644 index 0000000..37423e4 --- /dev/null +++ b/ereproxy_config.erl @@ -0,0 +1,31 @@ +-module(ereproxy_config). +-export([config/0, select_destination/1]). + +-include("ereproxy_config.hrl"). + +config() -> + #cfg{listen = [{http, 80}, {https, 443}], + ssl_opts = [{certfile, "example/cert.pem"}, + {keyfile, "example/key.pem"}] + }. + +%% select_destination +select_destination(HostName) -> + case lists:keysearch(HostName, 1, destination_list()) of + {value, {HostName, Destination}} -> Destination; + _UnknownHostName -> destination_default() + end. + +destination_default() -> + {"192.168.130.35", 80}. + +destination_list() -> + [ + {"code.sotun.de", {"192.168.130.103", 80}}, + {"wave.sotun.de", {"192.168.130.111", 9898}} + | [{WWW ++ "kraut" ++ Dash ++ "computing." ++ TLD, + {"192.168.130.37", 80}} + || WWW <- ["", "www."], + Dash <- ["", "-"], + TLD <- ["com", "de", "net", "eu", "org", "at"] ] + ]. diff --git a/ereproxy_config.hrl b/ereproxy_config.hrl new file mode 100644 index 0000000..ad66949 --- /dev/null +++ b/ereproxy_config.hrl @@ -0,0 +1,6 @@ +-record(cfg, { + listen, % e.g. [{80, http}, {443, https}], + log_file = "/var/log/ereproxy", + max_peek = 65536, + ssl_opts = [] + }). diff --git a/ereproxy_log.erl b/ereproxy_log.erl new file mode 100644 index 0000000..fe5c28b --- /dev/null +++ b/ereproxy_log.erl @@ -0,0 +1,24 @@ +-module(ereproxy_log). +-export([start_link/1, log/1]). + +-include("ereproxy_config.hrl"). + +start_link(CfgMod) -> + case file:open((CfgMod:config())#cfg.log_file, [append]) of + {ok, LogFile} -> + Pid = spawn_link(?MODULE, log, [LogFile]), + true = register(?MODULE, Pid), + {ok, Pid}; + Error -> + error_logger:error_report( + [{?MODULE, ?LINE}, Error]), + {error, Error} + end. + +%% log: write all message to logfile (w/ timestamp) +log(FD) -> + receive + Msg -> + io:format(FD, "~w~n", [{erlang:localtime(), Msg}]), + log(FD) + end. diff --git a/ereproxy_server.erl b/ereproxy_server.erl new file mode 100644 index 0000000..ef59144 --- /dev/null +++ b/ereproxy_server.erl @@ -0,0 +1,131 @@ +-module(ereproxy_server). +-behavior(supervisor). +-export([start_link/1, init/1, start_link_acceptor/2, acceptor/2, con/2, pass_through/1]). + +-include("ereproxy_config.hrl"). + +%%% supervisor over all connections + +start_link(CfgMod) -> + supervisor:start_link(?MODULE, CfgMod). + +init(CfgMod) -> + Cfg = CfgMod:config(), + {ok, {{one_for_one, 10, 60}, + [{{?MODULE, Listen}, % id + {?MODULE, start_link_acceptor, [CfgMod, Listen]}, + permanent, brutal_kill, worker, + [?MODULE]} + || Listen <- Cfg#cfg.listen]}}. + +%%% transport helper +tp(tcp, setup, _Args) -> ok; +tp(tcp, setopts, Args) -> apply(inet, setopts, Args); +tp(tcp, peername, Args) -> apply(inet, peername, Args); +tp(tcp, Method, Args) -> apply(gen_tcp, Method, Args); +tp(ssl, accept, Args) -> apply(ssl, transport_accept, Args); +tp(ssl, setup, Args) -> apply(ssl, ssl_accept, Args); +tp(ssl, Method, Args) -> apply(ssl, Method, Args). + +tp_c2t(tcp_closed) -> tcp; +tp_c2t(ssl_closed) -> ssl. + +tp_t2c(tcp) -> tcp_closed; +tp_t2c(ssl) -> ssl_closed. + +%%% listen and accept on a single port + +start_link_acceptor(CfgMod, {Method, ListenPort}) -> + DefaultOpts = [binary, {packet, 0}, {active, false}, {reuseaddr, true}], + {TP, Opts} = + case Method of + http -> {tcp, DefaultOpts}; + https -> {ssl, (CfgMod:config())#cfg.ssl_opts ++ DefaultOpts} + end, + case tp(TP, listen, [ListenPort, Opts]) of + {ok, Sock} -> + {ok, spawn_link(?MODULE, acceptor, [CfgMod, {TP, Sock}])}; + Error -> + error_logger:error_report( + [{?MODULE, ?LINE}, Error]), + {error, Error} + end. + +%% accept new connections and start a thread for them +acceptor(CfgMod, {TP, ListenSock}) -> + {ok, Sock} = tp(TP, accept, [ListenSock]), + Worker = spawn(?MODULE, con, [CfgMod, {TP, Sock}]), + ok = tp(TP, controlling_process, [Sock, Worker]), + ?MODULE:acceptor(CfgMod, {TP, ListenSock}). % allow code update + +%% con: care for connection until destination is known +con(CfgMod, ClientCon = {TP, ClientSock}) -> + MaxCache = (CfgMod:config())#cfg.max_peek, + ok = tp(TP, setup, [ClientSock]), + ok = tp(TP, setopts, [ClientSock, [{active, once}]]), + % get the hostname from http header + {ConHead, HostName} = parse_header(MaxCache, ClientCon), + {Host, Port} = CfgMod:select_destination(HostName), + % connect to destination + {ok, ServerSock} = gen_tcp:connect(Host, Port, [binary, {packet, 0}, {active, once}]), + % log redirection details + {ok, {SrcAddr, SrcPort}} = tp(TP, peername, [ClientSock]), + {ok, {RedAddr, RedPort}} = inet:sockname(ServerSock), + {ok, {DstAddr, DstPort}} = inet:peername(ServerSock), + ereproxy_log ! {{SrcAddr, SrcPort}, {RedAddr, RedPort}, {DstAddr, DstPort}}, + % pass through connection + gen_tcp:send(ServerSock, ConHead), + pass_through([ClientCon, {tcp, ServerSock}]). + +%% parse_header: search for the "^Host: " field + +% stop if too much data has been allocated +parse_header(NegativeCache, _, _, _, _) when NegativeCache < 0 -> + cachelimit_exceeded; + +% receive some bytes +parse_header(MaxCache, Con = {TP, Sock}, OldData, CurrentLine, _RestData = <<>>) -> + TP_Close = tp_t2c(TP), + receive + {TP, Sock, Data} -> + ok = tp(TP, setopts, [Sock, [{active, once}]]), + parse_header(MaxCache - size(Data), Con, OldData, CurrentLine, Data); + {TP_Close, Sock} -> + connection_closed + end; + +% parse: end of header (-> no Host-directive found) (two versions: LF/CRLF) +parse_header(_, _, OldData, <<>>, RestData = <<10:8, _>>) -> + {<<OldData/binary, RestData/binary>>, no_host}; +parse_header(_, _, OldData, <<13:8>>, RestData = <<10:8, _>>) -> + {<<OldData/binary, 13:8, RestData/binary>>, no_host}; +% parse: end of line, host directive +parse_header(_, _, OldData, CurrentLine = <<"Host: ", HostName/binary>>, RestData = <<10:8, _/binary>>) -> + {<<OldData/binary, CurrentLine/binary, RestData/binary>>, string:to_lower(binary_to_list(HostName) -- [$\r])}; +% parse: end of line +parse_header(MaxCache, Con, OldData, Line, <<10:8, RestData/binary>>) -> + parse_header(MaxCache, Con, <<OldData/binary, Line/binary, 10:8>>, <<>>, RestData); +% parse: any char +parse_header(MaxCache, Con, OldData, Line, <<Char:1/binary, RestData/binary>>) -> + parse_header(MaxCache, Con, OldData, <<Line/binary, Char/binary>>, RestData). + +% init all params +parse_header(MaxCache, Con) -> + parse_header(MaxCache, Con, <<>>, <<>>, <<>>). + +%% connect client and destination server +pass_through(Cons) -> + receive + {STP, Src, Data} -> + [{DTP, Dst}] = lists:subtract(Cons, [{STP, Src}]), + ok = tp(STP, setopts, [Src, [{active, once}]]), + ok = tp(DTP, send, [Dst, Data]), + ?MODULE:pass_through(Cons); % allow code update + {STP_Closed, Src} -> + STP = tp_c2t(STP_Closed), + [{DTP, Dst}] = lists:subtract(Cons, [{STP, Src}]), + ok = tp(DTP, close, [Dst]), + connections_closed + % TODO: check if this is standart compliant; we close both + % sides if one side closes one channel! + end. diff --git a/example/cert.pem b/example/cert.pem new file mode 100644 index 0000000..139083a --- /dev/null +++ b/example/cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICQTCCAaoCCQDxb8amCxJuvTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJk +ZTETMBEGA1UECAwKU29tZS1TdGF0ZTEOMAwGA1UECgwFc290dW4xETAPBgNVBAMM +CHNvdHVuLmRlMR4wHAYJKoZIhvcNAQkBFg9jcnlwdG9Ac290dW4uZGUwHhcNMTIw +NjA0MTAxODM4WhcNMTQwNDAxMTAxODM4WjBlMQswCQYDVQQGEwJkZTETMBEGA1UE +CAwKU29tZS1TdGF0ZTEOMAwGA1UECgwFc290dW4xETAPBgNVBAMMCHNvdHVuLmRl +MR4wHAYJKoZIhvcNAQkBFg9jcnlwdG9Ac290dW4uZGUwgZ8wDQYJKoZIhvcNAQEB +BQADgY0AMIGJAoGBAM/1oUsRKpvgUNzVkY5kdtCnVtevqfjzHkxjDZSMWiGI9TJd +iJKelmua/5+y880fQ7KAgPxhUx1cCL9VEeg3m9Og0OdloxwVABeMJPRMk2a6vQ7N +nZ0qa6U0dHPX/A4Vainmhoo+fHWky+a9xhjir6RcyRal41cHvlNap69XgiELAgMB +AAEwDQYJKoZIhvcNAQEFBQADgYEAnGcUC6BSmNOCSboRl08OLLnUWDNeioiM9BBh +8UfIEF6JNmfoV33tJOcBZl4wrnWAxSPXir6g8QDD0wca94FM+cWJFIP2PLCA7aaL +5oAki15T7J5H41tFsz1LiUMz1sIgFAV53iO2PwbnXCGNi8AJr/5fxL26tbXHbKP4 +hi3wfxg= +-----END CERTIFICATE----- diff --git a/example/key.pem b/example/key.pem new file mode 100644 index 0000000..f3ff783 --- /dev/null +++ b/example/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDP9aFLESqb4FDc1ZGOZHbQp1bXr6n48x5MYw2UjFohiPUyXYiS +npZrmv+fsvPNH0OygID8YVMdXAi/VRHoN5vToNDnZaMcFQAXjCT0TJNmur0OzZ2d +KmulNHRz1/wOFWop5oaKPnx1pMvmvcYY4q+kXMkWpeNXB75TWqevV4IhCwIDAQAB +AoGBAJo7ROVklOJIShCOQEaH0erLwMd0G65ruNPUPrUmJo5qgddZsTl0boDd0qnB +UbmWb2HKll8XW0oSANbOI9rCq0jBuI2BUrF4apGrTuuWosGc4NcBD7y9g8kAJLqt +HcaIle8ap2tbhbyZd+Bp0FALs2igavXzmkuMaM4NMuOIIJ+ZAkEA7hunanP17b7a +rxp2NelvlmfuzGALGZ15vaiPRey1UFLxL2KHl+9v0sXXb4YU8kXVXbdb9Zdr0wAm +A26OKl5epQJBAN+WBrmP7ieSFb8YZH5WMtQy4/ZQHdVUBXKVlhgFuyz2hqW3ABHx +hU5IxYXKnakBxNJQGpCxjeSgHxlPfTy6oe8CQQCmgVYYXhDa+TypeEKzvpLWxcU6 +y+rXNcT9OJNAHaBJFEcukKMrPzeeV9UoWsXpCaaEC4XV/tZazd7HRZdKz4U1AkBf +EVGGsTZYSPtKJ7sDJO+z3nejkek9fd5bHFOXn0g5FBGogKlc987wvGyQONjUtdXU +fw7smzJ0FcljX7MmkUytAkEAjvzDs00CpcoDTsIKKna3HCMwOfnMHqTuH+4Ygf46 +Vn2//Q3QgerrG9WbIhuC3GlZ1IlZzxwwOf7d17fCl4hGxg== +-----END RSA PRIVATE KEY----- |