💾 Archived View for gemini.rmf-dev.com › repo › Vaati › rxproxy › files › 3d578cfbed97985d353a0cfda3… captured on 2022-07-16 at 17:09:01. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
0 /* See LICENSE file for copyright and license details. */
1 module proxy;
2 import std.socket : InternetAddress, Socket, SocketException, SocketSet, TcpSocket, SocketAcceptException;
3 import logger;
4 import client;
5 import std.format;
6 import std.algorithm : remove;
7 import std.file;
8 import std.json;
9 import std.stdio;
10 import std.exception;
11 import std.algorithm;
12 import core.time;
13 import core.thread;
14 import std.parallelism;
15 import std.conv;
16 import std.array;
17 import std.string;
18
19 /// Default port of the proxy server
20 const ushort DEFAULTPORT = 6787;
21
22 /// Default number of maximum connections on the proxy server
23 const ushort DEFAULTMAXCONNECTIONS = 512;
24
25 /// Number of maximum connections allowed for a process
26 const ushort MAXCONNECTIONS = 1024;
27
28 /// SOCKS5 proxy server
29 class Proxy {
30 private:
31
32 struct PortRange {
33 int start;
34 int end;
35 }
36
37 bool bound=false;
38 TcpSocket listener;
39 Client[] clients;
40 int maxConnections = DEFAULTMAXCONNECTIONS;
41 ushort port;
42 string[] blacklist;
43 string[] whitelist;
44 PortRange[] portBlacklist;
45 PortRange[] portWhitelist;
46 string[] clientBlacklist;
47 string[] clientWhitelist;
48 long timeoutEstablished;
49 long timeout;
50
51 bool isIn(string str1, string str2) {
52 long len = str2.length;
53 len--;
54 if(len<=0) {
55 if(len==0&&str2[0]=='*')
56 return true;
57 return false;
58 }
59 if(str2[0]=='*'&&str2[len]=='*') {
60 if(find(str1,str2[1..len]).length!=0) return true;
61 } else if(str2[0]=='*') {
62 if(str1.endsWith(str2[1..str2.length])) return true;
63 } else if(str2[len]=='*') {
64 if(str1.startsWith(str2[0..len])) return true;
65 } else if(str1==str2) {
66 return true;
67 }
68 return false;
69 }
70
71 bool isIn(int port, PortRange ports) {
72 return ports.start<=port && ports.end>=port;
73 }
74
75 PortRange stringToPort(string str) {
76 PortRange ports = {-1,-1};
77 auto index = indexOf(str,'-');
78 long len = str.length;
79 if(len<=0) return ports;
80 if(str=="*") {
81 ports.start=0;
82 ports.end=65_565;
83 } else if(isNumeric(str)) {
84 const int i = to!int(str);
85 if(i<0) {
86 ports.start=0;
87 ports.end=-i;
88 } else
89 ports.start=ports.end=i;
90 } else if(str[len-1]=='-' && isNumeric(str[0..len-1])) {
91 const int i = to!int(str[0..len-1]);
92 if(i>=0) {
93 ports.start=i;
94 ports.end=65_565;
95 }
96 } else if(index!=-1&&index==lastIndexOf(str,'-')&&isNumeric(str[0..index])&&
97 isNumeric(str[indexOf(str,'-')..str.length])) {
98 ports.start=to!int(str[0..index]);
99 ports.end=to!int(str[index+1..str.length]);
100 }
101 return ports;
102 }
103
104 public:
105 /// Initialize listener sockets and assign port to the default 6787
106 this() {
107 port = DEFAULTPORT;
108
109 listener = new TcpSocket();
110 assert(listener.isAlive);
111 listener.blocking = false;
112
113 }
114
115 /// Initialize the server by loading the config and binding the port
116 void initialize(string path) {
117 if(!loadConfig(path)) {
118 createConfig(path);
119 }
120 try {
121 listener.bind(new InternetAddress(port));
122 listener.listen(10);
123 Logger.info(format("Listening on port %!d(MISSING)",port));
124 bound=true;
125 } catch(SocketException) {
126 Logger.error(format("Failed to bind socket on port %!d(MISSING)", port));
127 }
128 }
129
130 /// Return timeout
131 long getTimeout(bool isEstablished) {
132 return isEstablished?timeoutEstablished:timeout;
133 }
134
135 /// Create config to path
136 bool createConfig(string path) {
137
138 JSONValue config = [ "Port": DEFAULTPORT ];
139 config["MaxConnections"] = DEFAULTMAXCONNECTIONS;
140 config["DisplayTime"] = true;
141 config["PrintLog"] = true;
142 config["WriteLog"] = false;
143 config["PathLog"] = "/var/log/rxproxy.log";
144 config["Timeout"] = ["Established":600,"Unestablished":5];
145 config["Whitelist"] = ["Client":"","Domain":"","Port":""];
146 config["Blacklist"] = ["Client":"","Domain":"","Port":""];
147 config["Whitelist"]["Client"] = JSONValue(["localhost"]);
148 config["Blacklist"]["Client"] = JSONValue(["*"]);
149 config["Whitelist"]["Domain"] = JSONValue(["youtube.googleapis.com"]);
150 config["Blacklist"]["Domain"] = JSONValue(["*googleapis.com"]);
151 config["Blacklist"]["Port"] = JSONValue(["*","1-65565"]);
152 config["Whitelist"]["Port"] = JSONValue(["80","443"]);
153 File f;
154 try {
155 f = File(path, "w");
156 f.write(config.toPrettyString());
157 } catch(ErrnoException) {
158 Logger.error("Failed to create config file");
159 return false;
160 }
161 Logger.info("Config file "~path~" created");
162 return true;
163 }
164
165 /// Load config file from path
166 bool loadConfig(string path) {
167 string data;
168 try {
169 data = readText(path);
170 } catch(FileException) {
171 Logger.error("Can't read config file "~path);
172 return false;
173 }
174 try {
175 JSONValue config = parseJSON(data);
176 port = cast(ushort)config["Port"].integer;
177 maxConnections = cast(ushort)config["MaxConnections"].integer;
178 Logger.setParameters(config["DisplayTime"].boolean, config["WriteLog"].boolean,
179 config["PrintLog"].boolean, config["PathLog"].str);
180 if(maxConnections>MAXCONNECTIONS) {
181 Logger.warning(format(
182 "Config file has a number of maximum connection above the allowed number. The value was set to %!d(MISSING)",
183 MAXCONNECTIONS));
184 maxConnections=MAXCONNECTIONS;
185 }
186
187 if("Timeout" !in config) {
188 timeout = config["Timeout"]["Unestablished"].integer;
189 timeoutEstablished = config["Timeout"]["Established"].integer;
190 }
191
192 if("Whitelist" !in config) {
193 Logger.warning("Config file "~path~" contains no whitelist");
194 } else {
195 foreach (JSONValue str; config["Whitelist"]["Domain"].array)
196 whitelist = whitelist ~ str.str;
197 foreach (JSONValue str; config["Whitelist"]["Client"].array)
198 clientWhitelist = clientWhitelist ~ str.str;
199 foreach (JSONValue str; config["Whitelist"]["Port"].array) {
200 const PortRange range = stringToPort(str.str);
201 if(range.end!=-1&&range.start!=-1) {
202 portWhitelist = portWhitelist ~ range;
203 }
204 }
205 }
206
207 if("Blacklist" !in config) {
208 Logger.warning("Config file "~path~" contains no blacklist");
209 } else {
210 foreach (JSONValue str; config["Blacklist"]["Domain"].array)
211 blacklist = blacklist ~ str.str;
212 foreach (JSONValue str; config["Blacklist"]["Client"].array)
213 clientBlacklist = clientBlacklist ~ str.str;
214 foreach (JSONValue str; config["Blacklist"]["Port"].array) {
215 const PortRange range = stringToPort(str.str);
216 if(range.end!=-1&&range.start!=-1) {
217 portBlacklist = portBlacklist ~ range;
218 }
219 }
220 }
221
222 } catch(JSONException) {
223 Logger.error("Config file "~path~" is invalid");
224 return false;
225 }
226 Logger.info("Config file "~path~" loaded");
227 return true;
228 }
229
230 /// Return true if the domain is blacklisted
231 bool isBlacklisted(string domain) {
232 auto list = whitelist.filter!(a => isIn(domain,a)).map!(a => a)().array();
233 if(list.length>0) return false;
234 list = blacklist.filter!(a => isIn(domain,a)).map!(a => a)().array();
235 return list.length>0;
236 }
237
238 /// Return true if the client is blacklisted
239 bool isClientBlacklisted(string domain) {
240 auto list = clientWhitelist.filter!(a => isIn(domain,a)).map!(a => a)().array();
241 if(list.length>0) return false;
242 list = clientBlacklist.filter!(a => isIn(domain,a)).map!(a => a)().array();
243 return list.length>0;
244 }
245
246 /// Return true if the port is blacklisted
247 bool isPortBlacklisted(int port) {
248 foreach (PortRange p; portWhitelist) {
249 if(isIn(port,p)) return false;
250 }
251 foreach (PortRange p; portBlacklist) {
252 if(isIn(port,p)) return true;
253 }
254 return false;
255 }
256
257 /// Listen for incoming connections
258 bool listen() {
259 if(!bound) return false;
260 auto socketSet = new SocketSet(maxConnections);
261 auto connectSet = new SocketSet(maxConnections);
262
263 while (true)
264 {
265 socketSet.add(listener);
266
267 foreach (client; clients) {
268 socketSet.add(client.socket);
269 if(client.isConnecting()) {
270 connectSet.add(client.remote);
271 }
272 if(client.isEstablished()) {
273 socketSet.add(client.remote);
274 }
275 }
276
277 const int ret = Socket.select(socketSet, connectSet, null, dur!"seconds"(1));
278 if(ret==-1) {
279 return false;
280 } else if(ret==0) {
281 socketSet.reset();
282 connectSet.reset();
283 continue;
284 }
285
286 for (size_t i = 0; i < clients.length; i++)
287 {
288 if (clients[i].shouldDisconnect()) {
289 clients[i].destroy();
290 clients = clients.remove(i);
291 i--;
292 continue;
293 }
294 if (clients[i].isConnecting() && connectSet.isSet(clients[i].remote)) {
295 clients[i].confirm();
296 }
297 if (clients[i].isEstablished() && socketSet.isSet(clients[i].remote))
298 {
299 ubyte[1024] buf;
300 auto datLength = clients[i].remote.receive(buf[]);
301 if (datLength == Socket.ERROR)
302 Logger.error("Connection error.");
303 else if (datLength != 0)
304 {
305 try {
306 clients[i].socket.send(buf[0..datLength]);
307 continue;
308 } catch(SocketException) {
309 Logger.error(format("Connection closed. (Total connections: %!d(MISSING))",clients.length));
310 }
311 }
312 clients[i].destroy();
313 clients = clients.remove(i);
314 i--;
315 continue;
316 }
317 if (socketSet.isSet(clients[i].socket))
318 {
319 ubyte[1024] buf;
320 auto datLength = clients[i].socket.receive(buf[]);
321
322 if (datLength == Socket.ERROR)
323 Logger.error("Connection error.");
324 else if (datLength != 0 && clients[i].feed(buf,datLength))
325 {
326 continue;
327 }
328 clients[i].destroy();
329 clients = clients.remove(i);
330 i--;
331 }
332 }
333
334 if (socketSet.isSet(listener))
335 {
336 Socket sn = null;
337 scope (failure)
338 {
339 Logger.error("Error accepting");
340
341 if (sn)
342 sn.close();
343 }
344 try {
345 sn = listener.accept();
346 } catch(SocketAcceptException e) {
347 socketSet.reset();
348 connectSet.reset();
349 Logger.error(format("Too much sockets. (Total connections: %!d(MISSING))",clients.length));
350 continue;
351 }
352
353 assert(sn.isAlive);
354 assert(listener.isAlive);
355 auto remote = sn.remoteAddress().toHostNameString();
356
357 if (isClientBlacklisted(remote)) {
358 Logger.warning(remote~" tried to connect but is blacklisted");
359 sn.close();
360 }
361 else if (clients.length < maxConnections-1)
362 {
363 Logger.info(format("Connection from %!s(MISSING) established. (Total connections: %!d(MISSING))",
364 sn.remoteAddress().toString(),clients.length));
365 sn.blocking = false;
366 clients ~= new Client(sn,this);
367 }
368 else
369 {
370 Logger.warning(format("Rejected connection from %!s(MISSING); too many connections.",
371 sn.remoteAddress().toString()));
372 sn.close();
373 assert(!sn.isAlive);
374 assert(listener.isAlive);
375 }
376 }
377
378 socketSet.reset();
379 connectSet.reset();
380 }
381 }
382 }
383