Skip to content

Commit fb49670

Browse files
committed
Add update check
1 parent 002a2f9 commit fb49670

File tree

1 file changed

+162
-56
lines changed

1 file changed

+162
-56
lines changed

src/main.py

Lines changed: 162 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import argparse
1111
import asyncio
12+
import json
1213
import logging
1314
import os
1415
import 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:
198199
class 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

Comments
 (0)