summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile9
-rw-r--r--README34
-rw-r--r--ereproxy.app8
-rw-r--r--ereproxy.erl28
-rw-r--r--ereproxy_config.erl31
-rw-r--r--ereproxy_config.hrl6
-rw-r--r--ereproxy_log.erl24
-rw-r--r--ereproxy_server.erl131
-rw-r--r--example/cert.pem15
-rw-r--r--example/key.pem15
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 $<
diff --git a/README b/README
new file mode 100644
index 0000000..53bbdef
--- /dev/null
+++ b/README
@@ -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-----
contact: Jan Huwald // Impressum