|
9 | 9 |
|
10 | 10 | import argparse |
11 | 11 | import asyncio |
| 12 | +import base64 |
12 | 13 | import json |
13 | 14 | import logging |
14 | 15 | import os |
@@ -56,6 +57,8 @@ def __init__(self): |
56 | 57 | self.host = "127.0.0.1" |
57 | 58 | self.port = 8881 |
58 | 59 | self.out_host = None |
| 60 | + self.username = None |
| 61 | + self.password = None |
59 | 62 | self.blacklist_file = "blacklist.txt" |
60 | 63 | self.fragment_method = "random" |
61 | 64 | self.domain_matching = "strict" |
@@ -526,6 +529,7 @@ def __init__( |
526 | 529 | self.statistics = statistics |
527 | 530 | self.logger = logger |
528 | 531 | self.out_host = self.config.out_host |
| 532 | + self.auth_enabled = config.username is not None and config.password is not None |
529 | 533 | self.active_connections: Dict[Tuple, ConnectionInfo] = {} |
530 | 534 | self.connections_lock = asyncio.Lock() |
531 | 535 | self.tasks: List[asyncio.Task] = [] |
@@ -560,6 +564,9 @@ async def handle_connection( |
560 | 564 | self.statistics.update_traffic(0, len(http_data)) |
561 | 565 | conn_info.traffic_out += len(http_data) |
562 | 566 |
|
| 567 | + if not await self._check_proxy_authorization(http_data, writer): |
| 568 | + return |
| 569 | + |
563 | 570 | if method == b"CONNECT": |
564 | 571 | await self._handle_https_connection( |
565 | 572 | reader, writer, host, port, conn_key, conn_info |
@@ -596,6 +603,57 @@ def _parse_http_request(self, http_data: bytes) -> Tuple[bytes, bytes, int]: |
596 | 603 |
|
597 | 604 | return method, host, port |
598 | 605 |
|
| 606 | + async def _check_proxy_authorization( |
| 607 | + self, http_data: bytes, writer: asyncio.StreamWriter |
| 608 | + ) -> bool: |
| 609 | + """Check proxy authorization""" |
| 610 | + |
| 611 | + if not self.auth_enabled: |
| 612 | + return True |
| 613 | + |
| 614 | + headers = http_data.split(b"\r\n") |
| 615 | + auth_header = None |
| 616 | + for line in headers: |
| 617 | + if line.lower().startswith(b"proxy-authorization:"): |
| 618 | + auth_header = line |
| 619 | + break |
| 620 | + |
| 621 | + if auth_header is None: |
| 622 | + await self._send_407_response(writer) |
| 623 | + return False |
| 624 | + |
| 625 | + parts = auth_header.split(b" ", 2) |
| 626 | + if len(parts) != 3 or parts[1].lower() != b"basic": |
| 627 | + await self._send_407_response(writer) |
| 628 | + return False |
| 629 | + |
| 630 | + try: |
| 631 | + decoded = base64.b64decode(parts[2].strip()).decode("utf-8") |
| 632 | + username, password = decoded.split(":", 1) |
| 633 | + except Exception: |
| 634 | + await self._send_407_response(writer) |
| 635 | + return False |
| 636 | + |
| 637 | + if username != self.config.username or password != self.config.password: |
| 638 | + await self._send_407_response(writer) |
| 639 | + return False |
| 640 | + |
| 641 | + return True |
| 642 | + |
| 643 | + async def _send_407_response(self, writer: asyncio.StreamWriter): |
| 644 | + """Send 407 Proxy Authentication Required response""" |
| 645 | + |
| 646 | + response = ( |
| 647 | + "HTTP/1.1 407 Proxy Authentication Required\r\n" |
| 648 | + 'Proxy-Authenticate: Basic realm="NoDPI Proxy"\r\n' |
| 649 | + "Content-Length: 0\r\n" |
| 650 | + "Connection: close\r\n\r\n" |
| 651 | + ) |
| 652 | + writer.write(response.encode()) |
| 653 | + await writer.drain() |
| 654 | + writer.close() |
| 655 | + await writer.wait_closed() |
| 656 | + |
599 | 657 | async def _handle_https_connection( |
600 | 658 | self, |
601 | 659 | reader: asyncio.StreamReader, |
@@ -1147,6 +1205,8 @@ def load_from_args(args) -> ProxyConfig: |
1147 | 1205 | config.host = args.host |
1148 | 1206 | config.port = args.port |
1149 | 1207 | config.out_host = args.out_host |
| 1208 | + config.username = args.auth_username |
| 1209 | + config.password = args.auth_password |
1150 | 1210 | config.blacklist_file = args.blacklist |
1151 | 1211 | config.fragment_method = args.fragment_method |
1152 | 1212 | config.domain_matching = args.domain_matching |
@@ -1326,6 +1386,14 @@ def parse_args(): |
1326 | 1386 | choices=["loose", "strict"], |
1327 | 1387 | help="Domain matching mode (strict by default)", |
1328 | 1388 | ) |
| 1389 | + |
| 1390 | + parser.add_argument( |
| 1391 | + "--auth-username", required=False, help="Username for proxy authentication" |
| 1392 | + ) |
| 1393 | + parser.add_argument( |
| 1394 | + "--auth-password", required=False, help="Password for proxy authentication" |
| 1395 | + ) |
| 1396 | + |
1329 | 1397 | parser.add_argument( |
1330 | 1398 | "--log-access", required=False, help="Path to the access control log" |
1331 | 1399 | ) |
|
0 commit comments