1 /**
2  * TCP port forwarder.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.demo.portforward.portforward;
15 
16 import ae.net.asockets;
17 import ae.sys.log;
18 import ae.utils.text;
19 
20 import std.ascii;
21 import std.stdio;
22 import std.string;
23 import std.conv;
24 import std.datetime;
25 import std.getopt;
26 import std.exception;
27 
28 Logger log, recordLog;
29 bool logData, record;
30 
31 class Connection
32 {
33 	TcpConnection outer, inner;
34 	static int counter;
35 	int index;
36 
37 	this(TcpConnection outer, string host, ushort port)
38 	{
39 		index = counter++;
40 		this.outer = outer;
41 		inner = new TcpConnection;
42 		inner.handleConnect = &onInnerConnect;
43 		inner.handleReadData = &onInnerData;
44 		inner.handleDisconnect = &onInnerDisconnect;
45 		inner.connect(host, port);
46 	}
47 
48 	void onInnerConnect()
49 	{
50 		log("Connected to " ~ remoteAddressString(inner));
51 		if (record) recordLog(format("%d C %d %s", Clock.currStdTime(), index, inner.remoteAddress()));
52 		outer.handleReadData = &onOuterData;
53 		outer.handleDisconnect = &onOuterDisconnect;
54 	}
55 
56 	void onOuterData(Data data)
57 	{
58 		if (logData) log(format("Outer connection from %s sent %d bytes:\n%s", outer.remoteAddress(), data.length, hexDump(data.unsafeContents)));
59 		if (record) recordLog(format("%d < %d %s", Clock.currStdTime(), index, hexEscape(data.unsafeContents)));
60 		inner.send(data);
61 	}
62 
63 	void onOuterDisconnect(string reason, DisconnectType type)
64 	{
65 		log("Outer connection from " ~ outer.remoteAddressString() ~ " disconnected: " ~ reason);
66 		if (record) recordLog(format("%d [ %d %s", Clock.currStdTime(), index, reason));
67 		if (type != DisconnectType.requested)
68 			inner.disconnect();
69 	}
70 
71 	void onInnerData(Data data)
72 	{
73 		if (logData) log(format("Inner connection to %s sent %d bytes:\n%s", inner.remoteAddress(), data.length, hexDump(data.unsafeContents)));
74 		if (record) recordLog(format("%d > %d %s", Clock.currStdTime(), index, hexEscape(data.unsafeContents)));
75 		outer.send(data);
76 	}
77 
78 	void onInnerDisconnect(string reason, DisconnectType type)
79 	{
80 		log("Inner connection to " ~ remoteAddressString(inner) ~ " disconnected: " ~ reason);
81 		if (record) recordLog(format("%d ] %d %s", Clock.currStdTime(), index, reason));
82 		if (type != DisconnectType.requested)
83 			outer.disconnect();
84 	}
85 }
86 
87 class PortForwarder
88 {
89 	ushort localPort;
90 	string remoteHost;
91 	ushort remotePort;
92 
93 	this(string localHost, ushort localPort, string remoteHost, ushort remotePort)
94 	{
95 		this.localPort = localPort;
96 		this.remoteHost = remoteHost;
97 		this.remotePort = remotePort;
98 
99 		auto listener = new TcpServer();
100 		listener.handleAccept = &onAccept;
101 		listener.listen(localPort, localHost);
102 		log(format("Created forwarder: %s:%d -> %s:%d", localHost ? localHost : "*", localPort, remoteHost, remotePort));
103 		if (record) recordLog(format("%d L %d", Clock.currStdTime(), localPort));
104 	}
105 
106 	void onAccept(TcpConnection incoming)
107 	{
108 		log(format("Accepted connection from %s on port %d, forwarding to %s:%d", incoming.remoteAddressString(), localPort, remoteHost, remotePort));
109 		if (record) recordLog(format("%d A %d %d", Clock.currStdTime(), Connection.counter, localPort));
110 		new Connection(incoming, remoteHost, remotePort);
111 	}
112 }
113 
114 void main(string[] args)
115 {
116 	string listenOn = null;
117 	bool quiet = false;
118 	getopt(args,
119 		std.getopt.config.bundling,
120 		"l|listen", &listenOn,
121 		"v|verbose", &logData,
122 		"r|record", &record,
123 		"q|quiet", &quiet);
124 
125 	if (args.length < 2)
126 	{
127 		stderr.writefln("Usage: %s [OPTION]... <destination> <sourceport>[:<targetport>] [...]", args[0]);
128 		stderr.writeln("Supported options:");
129 		stderr.writeln(" -q  --quiet           Don't log to screen");
130 		stderr.writeln(" -l  --listen=ADDRESS  Listen on specified interface (all interfaces by default)");
131 		stderr.writeln(" -v  --verbose         Log sent/received data as well");
132 		stderr.writeln(" -r  --record          Log connections to a machine-readable format");
133 		return;
134 	}
135 
136 	log = createLogger("PortForward");
137 	if (record)
138 	{
139 		recordLog = rawFileLogger("PortForwardRecord", true);
140 		recordLog(format("%d S", Clock.currStdTime()));
141 	}
142 
143 	string destination = args[1];
144 	foreach (portPair; args[2..$])
145 	{
146 		auto segments = portPair.split(":");
147 		enforce(segments.length<=2, "Bad port syntax");
148 		new PortForwarder(listenOn, to!ushort(segments[0]), destination, to!ushort(segments.length==2 ? segments[1] : segments[0]));
149 	}
150 	socketManager.loop();
151 }
152 
153 string hexEscape(const(void)[] data)
154 {
155 	auto bytes = cast(const(ubyte)[])data;
156 	string s;
157 	foreach (b; bytes)
158 		if (b<0x20 || b>0x7E || b=='\\')
159 			s ~= `\` ~ hexDigits[b>>4] ~ hexDigits[b&15];
160 		else
161 			s ~= cast(char)b;
162 	return s;
163 }
164 
165 string remoteAddressString(TcpConnection c)
166 {
167 	try
168 	{
169 		auto remoteAddress = c.remoteAddress();
170 		return remoteAddress ? remoteAddress.toString()  : "(null)";
171 	}
172 	catch (Exception e)
173 		return "(error)";
174 }