99
1010import argparse
1111import asyncio
12+ import json
1213import logging
1314import os
1415import random
@@ -166,14 +167,14 @@ def load_blacklist(self) -> None:
166167
167168 with open (self .blacklist_file , "r" , encoding = "utf-8" , errors = "ignore" ) as f :
168169 for line in f :
169- if len (line .strip ()) < 2 or line .strip ()[0 ] == '#' :
170+ if len (line .strip ()) < 2 or line .strip ()[0 ] == "#" :
170171 continue
171- self .blocked .append (line .strip ().lower ().replace (' www.' , '' ))
172+ self .blocked .append (line .strip ().lower ().replace (" www." , "" ))
172173
173174 def is_blocked (self , domain : str ) -> bool :
174175 """Check if domain is in blacklist"""
175176
176- domain = domain .replace (' www.' , '' )
177+ domain = domain .replace (" www." , "" )
177178
178179 if self .config .domain_matching == "loose" :
179180 for blocked_domain in self .blocked :
@@ -183,9 +184,9 @@ def is_blocked(self, domain: str) -> bool:
183184 if domain in self .blocked :
184185 return True
185186
186- parts = domain .split ('.' )
187+ parts = domain .split ("." )
187188 for i in range (1 , len (parts )):
188- parent_domain = '.' .join (parts [i :])
189+ parent_domain = "." .join (parts [i :])
189190 if parent_domain in self .blocked :
190191 return True
191192
@@ -198,7 +199,10 @@ async def check_domain(self, domain: bytes) -> None:
198199class AutoBlacklistManager (IBlacklistManager ):
199200 """Blacklist manager that automatically detects blocked domains"""
200201
201- def __init__ (self , config : ProxyConfig ,):
202+ def __init__ (
203+ self ,
204+ config : ProxyConfig ,
205+ ):
202206
203207 self .blacklist_file = config .blacklist_file
204208 self .blocked : List [str ] = []
@@ -608,7 +612,9 @@ async def _handle_https_connection(
608612 conn_info .traffic_in += response_size
609613
610614 remote_reader , remote_writer = await asyncio .open_connection (
611- host .decode (), port , local_addr = (self .out_host , 0 ) if self .out_host else None
615+ host .decode (),
616+ port ,
617+ local_addr = (self .out_host , 0 ) if self .out_host else None ,
612618 )
613619
614620 writer .write (b"HTTP/1.1 200 Connection Established\r \n \r \n " )
@@ -630,7 +636,9 @@ async def _handle_http_connection(
630636 """Handle HTTP request"""
631637
632638 remote_reader , remote_writer = await asyncio .open_connection (
633- host .decode (), port , local_addr = (self .out_host , 0 ) if self .out_host else None
639+ host .decode (),
640+ port ,
641+ local_addr = (self .out_host , 0 ) if self .out_host else None ,
634642 )
635643
636644 remote_writer .write (http_data )
@@ -645,10 +653,13 @@ def _extract_sni_position(self, data):
645653 i = 0
646654 while i < len (data ) - 8 :
647655 if all (data [i + j ] == 0x00 for j in [0 , 1 , 2 , 4 , 6 , 7 ]):
648- ext_len = data [i + 3 ]
649- server_name_list_len = data [i + 5 ]
650- server_name_len = data [i + 8 ]
651- if ext_len - server_name_list_len == 2 and server_name_list_len - server_name_len == 3 :
656+ ext_len = data [i + 3 ]
657+ server_name_list_len = data [i + 5 ]
658+ server_name_len = data [i + 8 ]
659+ if (
660+ ext_len - server_name_list_len == 2
661+ and server_name_list_len - server_name_len == 3
662+ ):
652663 sni_start = i + 9
653664 sni_end = sni_start + server_name_len
654665 return sni_start , sni_end
@@ -697,30 +708,30 @@ async def _handle_initial_tls_data(
697708 sni_pos = self ._extract_sni_position (data )
698709
699710 if sni_pos :
700- part_start = data [:sni_pos [0 ]]
701- sni_data = data [sni_pos [0 ]:sni_pos [1 ]]
711+ part_start = data [: sni_pos [0 ]]
712+ sni_data = data [sni_pos [0 ]: sni_pos [1 ]]
702713 part_end = data [sni_pos [1 ]:]
703714 middle = (len (sni_data ) + 1 ) // 2
704715
705716 parts .append (
706- bytes .fromhex ("160304" ) +
707- len (part_start ).to_bytes (2 , "big" ) +
708- part_start
717+ bytes .fromhex ("160304" )
718+ + len (part_start ).to_bytes (2 , "big" )
719+ + part_start
709720 )
710721 parts .append (
711- bytes .fromhex ("160304" ) +
712- len (sni_data [:middle ]).to_bytes (2 , "big" ) +
713- sni_data [:middle ]
722+ bytes .fromhex ("160304" )
723+ + len (sni_data [:middle ]).to_bytes (2 , "big" )
724+ + sni_data [:middle ]
714725 )
715726 parts .append (
716- bytes .fromhex ("160304" ) +
717- len (sni_data [middle :]).to_bytes (2 , "big" ) +
718- sni_data [middle :]
727+ bytes .fromhex ("160304" )
728+ + len (sni_data [middle :]).to_bytes (2 , "big" )
729+ + sni_data [middle :]
719730 )
720731 parts .append (
721- bytes .fromhex ("160304" ) +
722- len (part_end ).to_bytes (2 , "big" ) +
723- part_end
732+ bytes .fromhex ("160304" )
733+ + len (part_end ).to_bytes (2 , "big" )
734+ + part_end
724735 )
725736
726737 elif self .config .fragment_method == "random" :
@@ -881,23 +892,89 @@ def __init__(
881892 )
882893 self .server = None
883894
895+ self .update_check_task = None
896+ self .update_available = None
897+ self .update_event = asyncio .Event ()
898+
884899 logger .set_error_counter_callback (
885900 statistics .increment_error_connections )
886901
887- def print_banner (self ) -> None :
902+ async def check_for_updates (self ):
903+ """Check for updates"""
904+
905+ if self .config .quiet :
906+ return None
907+
908+ try :
909+ loop = asyncio .get_event_loop ()
910+
911+ def sync_check ():
912+ try :
913+ req = Request (
914+ "https://gvcoder09.github.io/nodpi_site/api/v1/update_info.json" ,
915+ )
916+ with urlopen (req , timeout = 3 ) as response :
917+ if response .status == 200 :
918+ data = json .loads (response .read ())
919+ latest_version = data .get ("nodpi" , "" ).get (
920+ "latest_version" , ""
921+ )
922+ if latest_version and latest_version != __version__ :
923+ return latest_version
924+ except (URLError , json .JSONDecodeError , Exception ):
925+ pass
926+ return None
927+
928+ latest_version = await loop .run_in_executor (None , sync_check )
929+ if latest_version :
930+ self .update_available = latest_version
931+ self .update_event .set ()
932+ return f"\033 [93m[UPDATE]: Available new version: v{ latest_version } \033 [97m"
933+ except Exception :
934+ pass
935+ finally :
936+ self .update_event .set ()
937+ return None
938+
939+ async def print_banner (self ) -> None :
888940 """Print startup banner"""
889941
942+ self .update_check_task = asyncio .create_task (self .check_for_updates ())
943+
944+ try :
945+ await asyncio .wait_for (self .update_event .wait (), timeout = 2.0 )
946+ except asyncio .TimeoutError :
947+ if self .update_check_task and not self .update_check_task .done ():
948+ self .update_check_task .cancel ()
949+ try :
950+ await self .update_check_task
951+ except asyncio .CancelledError :
952+ pass
953+
890954 self .logger .info ("\033 ]0;NoDPI\007 " )
891955
892956 if sys .platform == "win32" :
893- os .system ("mode con: lines=35 " )
957+ os .system ("mode con: lines=33 " )
894958
895959 if sys .stdout .isatty ():
896960 console_width = os .get_terminal_size ().columns
897961 else :
898962 console_width = 80
899963
900- disclaimer = """DISCLAIMER. The developer and/or supplier of this software shall not be liable for any loss or damage, including but not limited to direct, indirect, incidental, punitive or consequential damages arising out of the use of or inability to use this software, even if the developer or supplier has been advised of the possibility of such damages. The developer and/or supplier of this software shall not be liable for any legal consequences arising out of the use of this software. This includes, but is not limited to, violation of laws, rules or regulations, as well as any claims or suits arising out of the use of this software. The user is solely responsible for compliance with all applicable laws and regulations when using this software."""
964+ disclaimer = (
965+ "DISCLAIMER. The developer and/or supplier of this software "
966+ "shall not be liable for any loss or damage, including but "
967+ "not limited to direct, indirect, incidental, punitive or "
968+ "consequential damages arising out of the use of or inability "
969+ "to use this software, even if the developer or supplier has been "
970+ "advised of the possibility of such damages. The developer and/or "
971+ "supplier of this software shall not be liable for any legal "
972+ "consequences arising out of the use of this software. This includes, "
973+ "but is not limited to, violation of laws, rules or regulations, "
974+ "as well as any claims or suits arising out of the use of this software. "
975+ "The user is solely responsible for compliance with all applicable laws "
976+ "and regulations when using this software."
977+ )
901978 wrapped_text = textwrap .TextWrapper (width = 70 ).wrap (disclaimer )
902979
903980 left_padding = (console_width - 76 ) // 2
@@ -917,19 +994,28 @@ def print_banner(self) -> None:
917994 self .logger .info (
918995 "\033 [91m" + " " * left_padding + "╚" + "═" * 72 + "╝" + "\033 [0m"
919996 )
997+
920998 time .sleep (1 )
921- self .logger .info ('\033 [2J\033 [H' )
999+
1000+ update_message = None
1001+ if self .update_check_task and self .update_check_task .done ():
1002+ try :
1003+ update_message = self .update_check_task .result ()
1004+ except (asyncio .CancelledError , Exception ):
1005+ pass
1006+
1007+ self .logger .info ("\033 [2J\033 [H" )
9221008
9231009 self .logger .info (
9241010 """
925- \033 [92m ██████ █████ ██████████ ███████████ █████
926- ░░██████ ░░███ ░░███░░░░███ ░░███░░░░░███░░███
927- ░███░███ ░███ ██████ ░███ ░░███ ░███ ░███ ░███
928- ░███░░███░███ ███░░███ ░███ ░███ ░██████████ ░███
929- ░███ ░░██████ ░███ ░███ ░███ ░███ ░███░░░░░░ ░███
930- ░███ ░░█████ ░███ ░███ ░███ ███ ░███ ░███
931- █████ ░░█████░░██████ ██████████ █████ █████
932- ░░░░░ ░░░░░ ░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░\033 [0m
1011+ \033 [92m ██████ █████ ██████████ ███████████ █████
1012+ ░░██████ ░░███ ░░███░░░░███ ░░███░░░░░███░░███
1013+ ░███░███ ░███ ██████ ░███ ░░███ ░███ ░███ ░███
1014+ ░███░░███░███ ███░░███ ░███ ░███ ░██████████ ░███
1015+ ░███ ░░██████ ░███ ░███ ░███ ░███ ░███░░░░░░ ░███
1016+ ░███ ░░█████ ░███ ░███ ░███ ███ ░███ ░███
1017+ █████ ░░█████░░██████ ██████████ █████ █████
1018+ ░░░░░ ░░░░░ ░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░\033 [0m
9331019 """
9341020 )
9351021 self .logger .info (f"\033 [92mVersion: { __version__ } " .center (50 ))
@@ -939,6 +1025,10 @@ def print_banner(self) -> None:
9391025 )
9401026
9411027 self .logger .info ("\n " )
1028+
1029+ if update_message :
1030+ self .logger .info (update_message )
1031+
9421032 self .logger .info (
9431033 f"\033 [92m[INFO]:\033 [97m Proxy is running on { self .config .host } :{ self .config .port } at { datetime .now ().strftime ('%H:%M on %Y-%m-%d' )} "
9441034 )
@@ -1000,7 +1090,7 @@ async def run(self) -> None:
10001090 """Run the proxy server"""
10011091
10021092 if not self .config .quiet :
1003- self .print_banner ()
1093+ await self .print_banner ()
10041094
10051095 try :
10061096 self .server = await asyncio .start_server (
@@ -1151,12 +1241,15 @@ def manage_autostart(action: str = "install") -> None:
11511241 ["systemctl" , "--user" , "daemon-reload" ], check = True )
11521242
11531243 subprocess .run (
1154- ["systemctl" , "--user" , "enable" , service_name ], check = True )
1155- subprocess .run (["systemctl" , "--user" , "start" ,
1156- service_name ], check = True )
1244+ ["systemctl" , "--user" , "enable" , service_name ], check = True
1245+ )
1246+ subprocess .run (
1247+ ["systemctl" , "--user" , "start" , service_name ], check = True
1248+ )
11571249
11581250 print (
1159- f"\033 [92m[INFO]:\033 [97m Service installed and started: { service_name } " )
1251+ f"\033 [92m[INFO]:\033 [97m Service installed and started: { service_name } "
1252+ )
11601253 print ("\033 [93m[NOTE]:\033 [97m Service will auto-start on login" )
11611254
11621255 except subprocess .CalledProcessError as e :
@@ -1167,10 +1260,16 @@ def manage_autostart(action: str = "install") -> None:
11671260
11681261 elif action == "uninstall" :
11691262 try :
1170- subprocess .run (["systemctl" , "--user" , "stop" , service_name ],
1171- capture_output = True , check = True )
1172- subprocess .run (["systemctl" , "--user" , "disable" , service_name ],
1173- capture_output = True , check = True )
1263+ subprocess .run (
1264+ ["systemctl" , "--user" , "stop" , service_name ],
1265+ capture_output = True ,
1266+ check = True ,
1267+ )
1268+ subprocess .run (
1269+ ["systemctl" , "--user" , "disable" , service_name ],
1270+ capture_output = True ,
1271+ check = True ,
1272+ )
11741273
11751274 if service_file .exists ():
11761275 service_file .unlink ()
@@ -1198,9 +1297,7 @@ def parse_args():
11981297 parser .add_argument ("--host" , default = "127.0.0.1" , help = "Proxy host" )
11991298 parser .add_argument ("--port" , type = int ,
12001299 default = 8881 , help = "Proxy port" )
1201- parser .add_argument (
1202- "--out-host" , help = "Outgoing proxy host"
1203- )
1300+ parser .add_argument ("--out-host" , help = "Outgoing proxy host" )
12041301
12051302 blacklist_group = parser .add_mutually_exclusive_group ()
12061303 blacklist_group .add_argument (
@@ -1217,10 +1314,18 @@ def parse_args():
12171314 help = "Automatic detection of blocked domains" ,
12181315 )
12191316
1220- parser .add_argument ("--fragment-method" , default = "random" , choices = [
1221- "random" , "sni" ], help = "Fragmentation method (random by default)" )
1222- parser .add_argument ("--domain-matching" , default = "strict" ,
1223- choices = ["loose" , "strict" ], help = "Domain matching mode (strict by default)" )
1317+ parser .add_argument (
1318+ "--fragment-method" ,
1319+ default = "random" ,
1320+ choices = ["random" , "sni" ],
1321+ help = "Fragmentation method (random by default)" ,
1322+ )
1323+ parser .add_argument (
1324+ "--domain-matching" ,
1325+ default = "strict" ,
1326+ choices = ["loose" , "strict" ],
1327+ help = "Domain matching mode (strict by default)" ,
1328+ )
12241329 parser .add_argument (
12251330 "--log-access" , required = False , help = "Path to the access control log"
12261331 )
@@ -1268,7 +1373,8 @@ async def run(cls):
12681373 sys .exit (0 )
12691374 else :
12701375 print (
1271- "\033 [91m[ERROR]: Autostart works only in executable version\033 [0m" )
1376+ "\033 [91m[ERROR]: Autostart works only in executable version\033 [0m"
1377+ )
12721378 sys .exit (1 )
12731379
12741380 config = ConfigLoader .load_from_args (args )
@@ -1289,7 +1395,7 @@ async def run(cls):
12891395 except asyncio .CancelledError :
12901396 await proxy .shutdown ()
12911397 logger .info (
1292- "\n " * 6 + "\033 [92m[INFO]:\033 [97m Shutting down proxy..." )
1398+ "\n " * 6 + "\033 [92m[INFO]:\033 [97m Shutting down proxy..." )
12931399 try :
12941400 if sys .platform == "win32" :
12951401 os .system ("mode con: lines=3000" )
0 commit comments