summaryrefslogtreecommitdiff
path: root/ereproxy_server.erl
blob: ef59144cf1f2d61c2af45fc2c0988ed6e0064258 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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.
contact: Jan Huwald // Impressum