From 30da20f8a92085b7c18c41722de0b208f20a7cc4 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Fri, 30 May 2025 23:24:59 +0200 Subject: [PATCH 1/8] test merge workflow files --- .github/workflows/code-scan.yml | 36 ---------- .github/workflows/docker-images.yml | 57 ---------------- .github/workflows/main.yml | 100 ++++++++++++++++++++++++++++ .github/workflows/unittest.yml | 29 -------- .gitignore | 4 +- 5 files changed, 103 insertions(+), 123 deletions(-) delete mode 100644 .github/workflows/code-scan.yml delete mode 100644 .github/workflows/docker-images.yml create mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/unittest.yml diff --git a/.github/workflows/code-scan.yml b/.github/workflows/code-scan.yml deleted file mode 100644 index 3b97ded..0000000 --- a/.github/workflows/code-scan.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Python Code Scan - -on: - push: - branches: - - main - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install dependencies - run: | - pip install --no-cache-dir pylint bandit - pip install --no-cache-dir -r requirements.txt - pip install --no-cache-dir -r benchmark/requirements.txt - - - name: Run Pylint - run: | - PYTHONPATH=. pylint $(find . -name "*.py" | xargs) - - - name: Run Bandit - run: | - bandit -r . --exit-zero \ No newline at end of file diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml deleted file mode 100644 index 29db1f2..0000000 --- a/.github/workflows/docker-images.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build and Push Docker Image - -on: - workflow_run: - workflows: - - "Pylint Check" - - "Run Unit Tests" - types: - - completed - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to GitHub Container Registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - - name: Get version from version.py - id: get_version - run: | - VERSION=$(python -c "from pyproxy.utils.version import __version__; print(__version__)") - echo "VERSION=${VERSION}" >> $GITHUB_ENV - echo "Docker tag version: $VERSION" - - - name: Convert repository owner to lowercase - run: echo "REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - - - name: Build Docker image - run: | - docker build -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }} -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest . - - - name: Build Docker slim image - run: | - docker build -f Dockerfile.slim -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }}-slim -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest-slim . - - - name: Push Docker image - run: | - docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }}-slim - docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest-slim - docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }} - docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..00f882a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,100 @@ +name: cicd + +on: + push: + branches: + - main + tags: + - "*" + pull_request: + +jobs: + code-scan: + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: pip install --no-cache-dir -U pip black flake8 bandit + + - name: Lint with flake8 + run: flake8 pythonrunner + + - name: Check with black + run: black --check pythonrunner + + - name: Check with bandit + run: bandit -r pythonrunner + + unittest: + needs: code-scan + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Install build dependencies + run: pip install --no-cache-dir -r requirements.txt + + - name: Run tests + run: python -m unittest discover -s tests + + build-docker: + needs: unittest + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Get version from version.py + id: get_version + run: | + VERSION=$(python -c "from pyproxy.utils.version import __version__; print(__version__)") + echo "VERSION=${VERSION}" >> $GITHUB_ENV + echo "Docker tag version: $VERSION" + + - name: Convert repository owner to lowercase + run: echo "REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Build Docker image + run: docker build -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }} -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest . + + - name: Build Docker slim image + run: docker build -f Dockerfile.slim -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }}-slim -t ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest-slim . + + - name: Push Docker image + run: | + docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }}-slim + docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest-slim + docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }} + docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest \ No newline at end of file diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml deleted file mode 100644 index 63f70ef..0000000 --- a/.github/workflows/unittest.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Run Unit Tests - -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install dependencies - run: | - pip install --no-cache-dir -r requirements.txt - - - name: Run tests - run: python -m unittest discover -s tests diff --git a/.gitignore b/.gitignore index c9e7e3a..a6310ce 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,6 @@ config/* # Certs folder certs/*.key certs/*.pem -certs/ca/*.pem \ No newline at end of file +certs/ca/*.pem + +.vscode \ No newline at end of file From a6bc750b6b452b773135e566f8e53aa5aeff0bd2 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:02:19 +0200 Subject: [PATCH 2/8] flake8, bandit & black --- .dockerignore | 2 +- .flake8 | 3 + .github/workflows/main.yml | 6 +- .pylintrc | 2 - CHANGELOG.md | 13 +++ CONTRIBUTING.md | 4 +- benchmark/benchmark.py | 65 +++++++------ benchmark/utils/html.py | 60 ++++++++---- benchmark/utils/req.py | 7 +- pyproxy.py | 1 - pyproxy/handlers/client.py | 54 +++++++---- pyproxy/handlers/http.py | 67 ++++++++----- pyproxy/handlers/https.py | 89 +++++++++++------ pyproxy/modules/cancel_inspect.py | 17 ++-- pyproxy/modules/custom_header.py | 17 ++-- pyproxy/modules/filter.py | 45 +++++---- pyproxy/modules/shortcuts.py | 17 ++-- pyproxy/monitoring/__init__.py | 7 -- pyproxy/monitoring/web.py | 140 +++++++++++++-------------- pyproxy/server.py | 139 ++++++++++++++++---------- pyproxy/utils/args.py | 118 +++++++++++++++++----- pyproxy/utils/config.py | 76 +++++++++------ pyproxy/utils/crypto.py | 15 +-- pyproxy/utils/http_req.py | 14 ++- pyproxy/utils/logger.py | 12 ++- tests/modules/test_cancel_inspect.py | 8 +- tests/modules/test_custom_header.py | 22 ++--- tests/modules/test_filter.py | 45 ++++----- tests/modules/test_shortcuts.py | 14 ++- tests/utils/test_crypto.py | 23 ++--- tests/utils/test_http_req.py | 6 +- tests/utils/test_logger.py | 5 +- 32 files changed, 668 insertions(+), 445 deletions(-) create mode 100644 .flake8 delete mode 100644 .pylintrc diff --git a/.dockerignore b/.dockerignore index 5b66d82..60bc44e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,7 +19,7 @@ benchmark/ .gitignore .git .gitattributes -.pylintrc +.flake8 .readthedocs.yaml CHANGELOG.md CONTRIBUTING.md diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f6be84b --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E203, W503 +max-line-length = 100 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00f882a..400cbca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,13 +29,13 @@ jobs: run: pip install --no-cache-dir -U pip black flake8 bandit - name: Lint with flake8 - run: flake8 pythonrunner + run: flake8 pyproxy tests benchmark - name: Check with black - run: black --check pythonrunner + run: black --check pyproxy tests benchmark - name: Check with bandit - run: bandit -r pythonrunner + run: bandit -r pyproxy tests benchmark unittest: needs: code-scan diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 58ef988..0000000 --- a/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MESSAGES CONTROL] -disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments, R0801, no-name-in-module diff --git a/CHANGELOG.md b/CHANGELOG.md index 026af4a..e240e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [0.3.3] - 2025-05-30 +### Added +- Change pylint to flake8 +- Black scan +- Build and release python package + +## [0.3.2] - 2025-05-13 +### Added +- Proxy chaining +- List of authorized IPs +## Patch +- Boolean in config ini + ## [0.3.1] - 2025-04-26 ### Added - Add slim image diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f3b364..7bbd8b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,9 @@ Whether you're fixing bugs, writing tests, or improving documentation, all contr ## ✅ Development Guidelines - Follow [PEP8](https://peps.python.org/pep-0008/) coding style. -- We use **pylint** to ensure code quality: +- We use **flake8** to ensure code quality: ```bash - pylint $(find . -name "*.py" | xargs) + flake8 pyproxy tests benchmark ``` - Tests are written using **unittest**. To run all tests: ```bash diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index e5c6b9c..1d9bdcc 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -1,5 +1,5 @@ """ -This module provides a set of functions to benchmark the performance of a proxy server +This module provides a set of functions to benchmark the performance of a proxy server by comparing the response times for HTTP requests sent with and without the use of a proxy. """ @@ -12,6 +12,7 @@ from utils.req import send_request_with_proxy, send_request_without_proxy from utils.html import create_combined_html_report + def benchmark(url: str, proxy: str, num_requests: int) -> tuple: """ Benchmarks the performance of sending requests to the specified$ @@ -56,50 +57,50 @@ def benchmark(url: str, proxy: str, num_requests: int) -> tuple: "max_with_proxy": max(times_with_proxy), } - results = pd.DataFrame({ - 'Request Number': range(1, num_requests + 1), - 'Without Proxy': times_without_proxy, - 'With Proxy': times_with_proxy - }) + results = pd.DataFrame( + { + "Request Number": range(1, num_requests + 1), + "Without Proxy": times_without_proxy, + "With Proxy": times_with_proxy, + } + ) return stats, results + def main() -> None: """ - Main function to parse command-line arguments, run benchmarks, and generate the report. + Main function to parse command-line arguments, run benchmarks, and generate the report. It either benchmarks a single URL or a list of URLs from a file. - + Returns: None """ parser = argparse.ArgumentParser(description="Proxy performance benchmark.") parser.add_argument( - '--proxy-url', + "--proxy-url", type=str, default="http://localhost:8080", - help="The proxy URL to use" + help="The proxy URL to use", ) parser.add_argument( - '--target-url', + "--target-url", type=str, - help="A single URL to test (e.g., http://example.com)" + help="A single URL to test (e.g., http://example.com)", ) parser.add_argument( - '--target-file', + "--target-file", type=str, - help="A file containing a list of URLs to test" + help="A file containing a list of URLs to test", ) parser.add_argument( - '--num-requests', + "--num-requests", type=int, default=10, - help="Number of requests to send (default: 10)" + help="Number of requests to send (default: 10)", ) parser.add_argument( - '--output-dir', - type=str, - default="benchmark/outputs", - help="Output directory" + "--output-dir", type=str, default="benchmark/outputs", help="Output directory" ) args = parser.parse_args() @@ -118,7 +119,7 @@ def main() -> None: print(f"Error: the file {args.target_file} does not exist.") sys.exit(1) - with open(args.target_file, 'r', encoding="utf-8") as f: + with open(args.target_file, "r", encoding="utf-8") as f: urls = [line.strip() for line in f if line.strip()] for url in urls: @@ -133,26 +134,32 @@ def main() -> None: avg_with_proxy_list = [] for stats, _ in all_results.values(): - avg_without_proxy_list.append(stats['avg_without_proxy']) - avg_with_proxy_list.append(stats['avg_with_proxy']) + avg_without_proxy_list.append(stats["avg_without_proxy"]) + avg_with_proxy_list.append(stats["avg_with_proxy"]) global_avg_without_proxy = sum(avg_without_proxy_list) / len(avg_without_proxy_list) global_avg_with_proxy = sum(avg_with_proxy_list) / len(avg_with_proxy_list) percentage_change = ( - (global_avg_with_proxy - global_avg_without_proxy) / - global_avg_without_proxy + (global_avg_with_proxy - global_avg_without_proxy) / global_avg_without_proxy ) * 100 print(f"Global average without proxy: {global_avg_without_proxy:.6f} seconds") print(f"Global average with proxy: {global_avg_with_proxy:.6f} seconds") - print(f"Impact: {'Improvement' if percentage_change < 0 else 'Slowdown'} of " - f"{abs(percentage_change):.2f}%") + print( + f"Impact: {'Improvement' if percentage_change < 0 else 'Slowdown'} of " + f"{abs(percentage_change):.2f}%" + ) create_combined_html_report( - all_results, global_avg_without_proxy, global_avg_with_proxy, - percentage_change, args.output_dir, timestamp + all_results, + global_avg_without_proxy, + global_avg_with_proxy, + percentage_change, + args.output_dir, + timestamp, ) + if __name__ == "__main__": main() diff --git a/benchmark/utils/html.py b/benchmark/utils/html.py index fa708c3..fccfcd1 100644 --- a/benchmark/utils/html.py +++ b/benchmark/utils/html.py @@ -8,14 +8,15 @@ TEMPLATE_PATH = "benchmark/templates/report_template.html" + def generate_combined_table(all_results: dict) -> str: """ Generates a single HTML table combining statistics for all URLs with sub-columns for avg, min, and max. - + Args: all_results (dict): A dictionary containing the results for each URL. - + Returns: str: The HTML table as a string. """ @@ -64,14 +65,15 @@ def generate_combined_table(all_results: dict) -> str: return table_html + def prepare_filenames(output_dir: str, timestamp: str) -> dict: """ Prepares the filenames for the report and plotly files. - + Args: output_dir (str): The directory to save the report in. timestamp (str): The timestamp to use in filenames. - + Returns: dict: A dictionary containing the plotly and html file paths. """ @@ -83,10 +85,7 @@ def prepare_filenames(output_dir: str, timestamp: str) -> dict: plotly_filepath = os.path.join(output_dir, plotly_filename) html_filepath = os.path.join(output_dir, html_filename) - return { - "plotly": plotly_filepath, - "html": html_filepath - } + return {"plotly": plotly_filepath, "html": html_filepath} def render_template(template_path: str, context: dict) -> str: @@ -104,10 +103,17 @@ def render_template(template_path: str, context: dict) -> str: template = f.read() return template.format(**context) -def create_combined_html_report(all_results: dict, avg_without_proxy: float, avg_with_proxy: float, - percentage_change: float, output_dir: str, timestamp: str) -> None: + +def create_combined_html_report( + all_results: dict, + avg_without_proxy: float, + avg_with_proxy: float, + percentage_change: float, + output_dir: str, + timestamp: str, +) -> None: """ - Generates an HTML report with the benchmark results, including graphs and statistics. + Generates an HTML report with the benchmark results, including graphs and statistics. Saves the report to the specified output directory. Args: @@ -118,7 +124,7 @@ def create_combined_html_report(all_results: dict, avg_without_proxy: float, avg between requests with and without a proxy. output_dir (str): The directory to save the report in. timestamp (str): The timestamp to use in filenames. - + Returns: None """ @@ -127,14 +133,28 @@ def create_combined_html_report(all_results: dict, avg_without_proxy: float, avg filenames = prepare_filenames(output_dir, timestamp) for url, (_, results) in all_results.items(): - fig.add_trace(go.Scatter(x=results['Request Number'], y=results['Without Proxy'], - mode='lines+markers', name=f'Without Proxy - {url}')) - fig.add_trace(go.Scatter(x=results['Request Number'], y=results['With Proxy'], - mode='lines+markers', name=f'With Proxy - {url}')) - - fig.update_layout(title="Response Time per Request (All URLs)", - xaxis_title="Request Number", - yaxis_title="Response Time (seconds)") + fig.add_trace( + go.Scatter( + x=results["Request Number"], + y=results["Without Proxy"], + mode="lines+markers", + name=f"Without Proxy - {url}", + ) + ) + fig.add_trace( + go.Scatter( + x=results["Request Number"], + y=results["With Proxy"], + mode="lines+markers", + name=f"With Proxy - {url}", + ) + ) + + fig.update_layout( + title="Response Time per Request (All URLs)", + xaxis_title="Request Number", + yaxis_title="Response Time (seconds)", + ) fig.write_html(filenames["plotly"]) diff --git a/benchmark/utils/req.py b/benchmark/utils/req.py index e7c97f0..a6a9163 100644 --- a/benchmark/utils/req.py +++ b/benchmark/utils/req.py @@ -6,9 +6,10 @@ import time import requests + def send_request_without_proxy(url: str) -> float: """ - Sends an HTTP GET request to the provided URL without using a proxy, + Sends an HTTP GET request to the provided URL without using a proxy, and measures the time it takes to complete the request. Args: @@ -25,7 +26,7 @@ def send_request_without_proxy(url: str) -> float: def send_request_with_proxy(url: str, proxy: str) -> float: """ - Sends an HTTP GET request to the provided URL using a proxy, + Sends an HTTP GET request to the provided URL using a proxy, and measures the time it takes to complete the request. Args: @@ -35,7 +36,7 @@ def send_request_with_proxy(url: str, proxy: str) -> float: Returns: float: The time taken to complete the request in seconds. """ - proxies = {'http': proxy, 'https': proxy} + proxies = {"http": proxy, "https": proxy} start_time = time.time() requests.get(url, proxies=proxies, timeout=10) end_time = time.time() diff --git a/pyproxy.py b/pyproxy.py index 0343d75..420b0a1 100644 --- a/pyproxy.py +++ b/pyproxy.py @@ -8,7 +8,6 @@ from pyproxy.utils.args import parse_args, load_config, get_config_value, str_to_bool from pyproxy.utils.config import ProxyConfigLogger, ProxyConfigFilter, ProxyConfigSSL -# pylint: disable=C0301,R0914 def main(): """ Main entry point of the proxy server. It parses command-line arguments, loads the configuration file, diff --git a/pyproxy/handlers/client.py b/pyproxy/handlers/client.py index 8c494f2..406bbd1 100644 --- a/pyproxy/handlers/client.py +++ b/pyproxy/handlers/client.py @@ -11,20 +11,38 @@ from pyproxy.handlers.http import HttpHandler from pyproxy.handlers.https import HttpsHandler -# pylint: disable=R0914,R0903 + class ProxyHandlers: """ ProxyHandlers manages client connections for a proxy server, handling both HTTP - and HTTPS requests. It processes request forwarding, blocking, SSL inspection, - and custom headers based on configuration settings. This class is responsible - for dispatching the correct handler for HTTP or HTTPS requests and managing + and HTTPS requests. It processes request forwarding, blocking, SSL inspection, + and custom headers based on configuration settings. This class is responsible + for dispatching the correct handler for HTTP or HTTPS requests and managing connection-related operations. """ - def __init__(self, html_403, logger_config, filter_config, ssl_config, - filter_queue, filter_result_queue, shortcuts_queue, shortcuts_result_queue, - cancel_inspect_queue, cancel_inspect_result_queue, custom_header_queue, - custom_header_result_queue, console_logger, shortcuts, custom_header, - active_connections, proxy_enable, proxy_host, proxy_port): + + def __init__( + self, + html_403, + logger_config, + filter_config, + ssl_config, + filter_queue, + filter_result_queue, + shortcuts_queue, + shortcuts_result_queue, + cancel_inspect_queue, + cancel_inspect_result_queue, + custom_header_queue, + custom_header_result_queue, + console_logger, + shortcuts, + custom_header, + active_connections, + proxy_enable, + proxy_host, + proxy_port, + ): self.html_403 = html_403 self.logger_config = logger_config self.filter_config = filter_config @@ -40,16 +58,16 @@ def __init__(self, html_403, logger_config, filter_config, ssl_config, self.console_logger = console_logger self.config_shortcuts = shortcuts self.config_custom_header = custom_header - self.proxy_enable=proxy_enable - self.proxy_host=proxy_host - self.proxy_port=proxy_port + self.proxy_enable = proxy_enable + self.proxy_host = proxy_host + self.proxy_port = proxy_port self.active_connections = active_connections def handle_client(self, client_socket): """ - Handles an incoming client connection by processing the request and forwarding - it to the appropriate handler based on whether the request is HTTP or HTTPS. - + Handles an incoming client connection by processing the request and forwarding + it to the appropriate handler based on whether the request is HTTP or HTTPS. + Args: client_socket (socket): The socket object for the client connection. """ @@ -61,7 +79,7 @@ def handle_client(self, client_socket): self.active_connections.pop(threading.get_ident(), None) return - first_line = request.decode(errors='ignore').split("\n")[0] + first_line = request.decode(errors="ignore").split("\n")[0] if first_line.startswith("CONNECT"): client_https_handler = HttpsHandler( @@ -83,7 +101,7 @@ def handle_client(self, client_socket): proxy_enable=self.proxy_enable, proxy_host=self.proxy_host, proxy_port=self.proxy_port, - active_connections=self.active_connections + active_connections=self.active_connections, ) client_https_handler.handle_https_connection(client_socket, first_line) else: @@ -103,6 +121,6 @@ def handle_client(self, client_socket): proxy_enable=self.proxy_enable, proxy_host=self.proxy_host, proxy_port=self.proxy_port, - active_connections=self.active_connections + active_connections=self.active_connections, ) client_http_handler.handle_http_request(client_socket, request) diff --git a/pyproxy/handlers/http.py b/pyproxy/handlers/http.py index 7d260cb..a71612d 100644 --- a/pyproxy/handlers/http.py +++ b/pyproxy/handlers/http.py @@ -11,17 +11,33 @@ from pyproxy.utils.http_req import extract_headers, parse_url -# pylint: disable=R0914 + class HttpHandler: """ HttpHandler manages client HTTP connections for a proxy server, handling request forwarding, filtering, blocking, and custom header modification based on configuration settings. """ - def __init__(self, html_403, logger_config, filter_config, - filter_queue, filter_result_queue, shortcuts_queue, shortcuts_result_queue, - custom_header_queue, custom_header_result_queue, console_logger, shortcuts, - custom_header, active_connections, proxy_enable, proxy_host, proxy_port): + + def __init__( + self, + html_403, + logger_config, + filter_config, + filter_queue, + filter_result_queue, + shortcuts_queue, + shortcuts_result_queue, + custom_header_queue, + custom_header_result_queue, + console_logger, + shortcuts, + custom_header, + active_connections, + proxy_enable, + proxy_host, + proxy_port, + ): self.html_403 = html_403 self.logger_config = logger_config self.filter_config = filter_config @@ -34,25 +50,25 @@ def __init__(self, html_403, logger_config, filter_config, self.console_logger = console_logger self.config_shortcuts = shortcuts self.config_custom_header = custom_header - self.proxy_enable=proxy_enable - self.proxy_host=proxy_host - self.proxy_port=proxy_port + self.proxy_enable = proxy_enable + self.proxy_host = proxy_host + self.proxy_port = proxy_port self.active_connections = active_connections def handle_http_request(self, client_socket, request): """ Processes an HTTP request, checks for URL filtering, applies shortcuts, and forwards the request to the target server if not blocked. - + Args: client_socket (socket): The socket object for the client connection. request (bytes): The raw HTTP request sent by the client. """ - first_line = request.decode(errors='ignore').split("\n")[0] + first_line = request.decode(errors="ignore").split("\n")[0] url = first_line.split(" ")[1] if self.config_custom_header and os.path.isfile(self.config_custom_header): - headers = extract_headers(request.decode(errors='ignore')) + headers = extract_headers(request.decode(errors="ignore")) self.custom_header_queue.put(url) new_headers = self.custom_header_result_queue.get(timeout=5) headers.update(new_headers) @@ -80,12 +96,9 @@ def handle_http_request(self, client_socket, request): if result[1] == "Blocked": if not self.logger_config.no_logging_block: self.logger_config.block_logger.info( - "%s - %s - %s", - client_socket.getpeername()[0], - url, - first_line + "%s - %s - %s", client_socket.getpeername()[0], url, first_line ) - with open(self.html_403, "r", encoding='utf-8') as f: + with open(self.html_403, "r", encoding="utf-8") as f: custom_403_page = f.read() response = ( f"HTTP/1.1 403 Forbidden\r\n" @@ -103,22 +116,24 @@ def handle_http_request(self, client_socket, request): "%s - %s - %s", client_socket.getpeername()[0], f"http://{server_host}", - first_line + first_line, ) if self.config_custom_header and os.path.isfile(self.config_custom_header): - request_lines = request.decode(errors='ignore').split("\r\n") + request_lines = request.decode(errors="ignore").split("\r\n") request_line = request_lines[0] # GET / HTTP/1.1 header_lines = [f"{key}: {value}" for key, value in headers.items()] reconstructed_headers = "\r\n".join(header_lines) - if "\r\n\r\n" in request.decode(errors='ignore'): - body = request.decode(errors='ignore').split("\r\n\r\n", 1)[1] + if "\r\n\r\n" in request.decode(errors="ignore"): + body = request.decode(errors="ignore").split("\r\n\r\n", 1)[1] else: body = "" - modified_request = f"{request_line}\r\n{reconstructed_headers}\r\n\r\n{body}".encode() + modified_request = ( + f"{request_line}\r\n{reconstructed_headers}\r\n\r\n{body}".encode() + ) self.forward_request_to_server(client_socket, modified_request, url) @@ -128,7 +143,7 @@ def handle_http_request(self, client_socket, request): def forward_request_to_server(self, client_socket, request, url): """ Forwards the HTTP request to the target server and sends the response back to the client. - + Args: client_socket (socket): The socket object for the client connection. request (bytes): The raw HTTP request sent by the client. @@ -156,13 +171,17 @@ def forward_request_to_server(self, client_socket, request, url): response = server_socket.recv(4096) if response: client_socket.send(response) - self.active_connections[thread_id]["bytes_received"] += len(response) + self.active_connections[thread_id]["bytes_received"] += len( + response + ) else: break except socket.timeout: break except (socket.timeout, socket.gaierror, ConnectionRefusedError, OSError) as e: - self.console_logger.error("Error connecting to the server %s : %s", server_host, e) + self.console_logger.error( + "Error connecting to the server %s : %s", server_host, e + ) response = ( f"HTTP/1.1 502 Bad Gateway\r\n" f"Content-Length: {len('Bad Gateway')} \r\n" diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index ffdb1f8..d01f50e 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -14,7 +14,7 @@ from pyproxy.utils.crypto import generate_certificate -# pylint: disable=R0914 + class HttpsHandler: """ Handles HTTPS client connections for a proxy server. @@ -23,11 +23,29 @@ class HttpsHandler: processes HTTPS `CONNECT` requests and either tunnels them directly to the destination or performs SSL interception for inspection and filtering. """ - def __init__(self, html_403, logger_config, filter_config, ssl_config, - filter_queue, filter_result_queue, shortcuts_queue, shortcuts_result_queue, - cancel_inspect_queue, cancel_inspect_result_queue, custom_header_queue, - custom_header_result_queue, console_logger, shortcuts, custom_header, - active_connections, proxy_enable, proxy_host, proxy_port): + + def __init__( + self, + html_403, + logger_config, + filter_config, + ssl_config, + filter_queue, + filter_result_queue, + shortcuts_queue, + shortcuts_result_queue, + cancel_inspect_queue, + cancel_inspect_result_queue, + custom_header_queue, + custom_header_result_queue, + console_logger, + shortcuts, + custom_header, + active_connections, + proxy_enable, + proxy_host, + proxy_port, + ): self.html_403 = html_403 self.logger_config = logger_config self.filter_config = filter_config @@ -43,17 +61,16 @@ def __init__(self, html_403, logger_config, filter_config, ssl_config, self.console_logger = console_logger self.config_shortcuts = shortcuts self.config_custom_header = custom_header - self.proxy_enable=proxy_enable - self.proxy_host=proxy_host - self.proxy_port=proxy_port + self.proxy_enable = proxy_enable + self.proxy_host = proxy_host + self.proxy_port = proxy_port self.active_connections = active_connections - # pylint: disable=too-many-locals,too-many-statements,too-many-branches,too-many-nested-blocks def handle_https_connection(self, client_socket, first_line): """ - Handles HTTPS connections by establishing a connection with the target server + Handles HTTPS connections by establishing a connection with the target server and relaying data between the client and server. - + Args: client_socket (socket): The socket object for the client connection. first_line (str): The first line of the CONNECT request from the client. @@ -71,9 +88,9 @@ def handle_https_connection(self, client_socket, first_line): "%s - %s - %s", client_socket.getpeername()[0], target, - first_line + first_line, ) - with open(self.html_403, "r", encoding='utf-8') as f: + with open(self.html_403, "r", encoding="utf-8") as f: custom_403_page = f.read() response = ( f"HTTP/1.1 403 Forbidden\r\n" @@ -100,27 +117,26 @@ def handle_https_connection(self, client_socket, first_line): server_host, self.ssl_config.inspect_certs_folder, self.ssl_config.inspect_ca_cert, - self.ssl_config.inspect_ca_key + self.ssl_config.inspect_ca_key, ) client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) client_context.load_cert_chain(certfile=cert_path, keyfile=key_path) client_context.options |= ( - ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | - ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 ) client_context.load_verify_locations(self.ssl_config.inspect_ca_cert) try: client_socket.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n") ssl_client_socket = client_context.wrap_socket( - client_socket, - server_side=True, - do_handshake_on_connect=False + client_socket, server_side=True, do_handshake_on_connect=False ) ssl_client_socket.do_handshake() if self.proxy_enable: - next_proxy_socket = socket.create_connection((self.proxy_host, self.proxy_port)) + next_proxy_socket = socket.create_connection( + (self.proxy_host, self.proxy_port) + ) connect_command = ( f"CONNECT {server_host}:{server_port} HTTP/1.1\r\n" f"Host: {server_host}:{server_port}\r\n\r\n" @@ -151,7 +167,7 @@ def handle_https_connection(self, client_socket, first_line): ssl_server_socket = server_context.wrap_socket( server_socket, server_hostname=server_host, - do_handshake_on_connect=True + do_handshake_on_connect=True, ) try: @@ -170,9 +186,9 @@ def handle_https_connection(self, client_socket, first_line): "%s - %s - %s", ssl_client_socket.getpeername()[0], target, - first_line + first_line, ) - with open(self.html_403, "r", encoding='utf-8') as f: + with open(self.html_403, "r", encoding="utf-8") as f: custom_403_page = f.read() response = ( f"HTTP/1.1 403 Forbidden\r\n" @@ -191,7 +207,7 @@ def handle_https_connection(self, client_socket, first_line): ssl_client_socket.getpeername()[0], f"https://{server_host}", method, - full_url + full_url, ) ssl_server_socket.sendall(first_request.encode()) @@ -224,11 +240,18 @@ def handle_https_connection(self, client_socket, first_line): "%s - %s - %s", client_socket.getpeername()[0], f"https://{server_host}", - first_line + first_line, ) self.transfer_data_between_sockets(client_socket, server_socket) - except (socket.timeout, socket.gaierror, ConnectionRefusedError, OSError) as e: - self.console_logger.error("Error connecting to the server %s: %s", server_host, e) + except ( + socket.timeout, + socket.gaierror, + ConnectionRefusedError, + OSError, + ) as e: + self.console_logger.error( + "Error connecting to the server %s: %s", server_host, e + ) response = ( f"HTTP/1.1 502 Bad Gateway\r\n" f"Content-Length: {len('Bad Gateway')} \r\n" @@ -241,7 +264,7 @@ def handle_https_connection(self, client_socket, first_line): def transfer_data_between_sockets(self, client_socket, server_socket): """ Transfers data between the client socket and server socket. - + Args: client_socket (socket): The socket object for the client connection. server_socket (socket): The socket object for the server connection. @@ -250,8 +273,8 @@ def transfer_data_between_sockets(self, client_socket, server_socket): thread_id = threading.get_ident() if ( - thread_id in self.active_connections and - "target_ip" not in self.active_connections[thread_id] + thread_id in self.active_connections + and "target_ip" not in self.active_connections[thread_id] ): try: target_ip, target_port = server_socket.getpeername() @@ -276,7 +299,9 @@ def transfer_data_between_sockets(self, client_socket, server_socket): self.active_connections[thread_id]["bytes_sent"] += len(data) else: client_socket.sendall(data) - self.active_connections[thread_id]["bytes_received"] += len(data) + self.active_connections[thread_id]["bytes_received"] += len( + data + ) except (socket.error, OSError): client_socket.close() server_socket.close() diff --git a/pyproxy/modules/cancel_inspect.py b/pyproxy/modules/cancel_inspect.py index 3e8b621..aeeb2ca 100644 --- a/pyproxy/modules/cancel_inspect.py +++ b/pyproxy/modules/cancel_inspect.py @@ -16,42 +16,41 @@ import sys import threading + def load_cancel_inspect(cancel_inspect_path: str) -> dict: """ Loads cancel inspection entries from a file into a list. - + Args: cancel_inspect_path (str): The path to the file containing the entries. - + Returns: list: A list containing each line (entry) from the file. """ cancel_inspect = [] - with open(cancel_inspect_path, 'r', encoding='utf-8') as f: + with open(cancel_inspect_path, "r", encoding="utf-8") as f: for line in f: cancel_inspect.append(line) return cancel_inspect -# pylint: disable=too-many-locals + def cancel_inspect_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - cancel_inspect_path: str + cancel_inspect_path: str, ) -> None: """ Process that monitors the cancel inspection file and checks if received entries exist in it. - + Args: queue (multiprocessing.Queue): A queue to receive entries to check. result_queue (multiprocessing.Queue): A queue to send back True/False depending on match. cancel_inspect_path (str): Path to the file containing cancel inspection entries. """ manager = multiprocessing.Manager() - cancel_inspect_data = manager.list( - load_cancel_inspect(cancel_inspect_path) - ) + cancel_inspect_data = manager.list(load_cancel_inspect(cancel_inspect_path)) error_event = threading.Event() diff --git a/pyproxy/modules/custom_header.py b/pyproxy/modules/custom_header.py index b224e12..993b411 100644 --- a/pyproxy/modules/custom_header.py +++ b/pyproxy/modules/custom_header.py @@ -17,37 +17,36 @@ import threading import json + def load_custom_header(custom_header_path: str) -> dict: """ Loads custom header entries from a file into a list. - + Args: custom_header_path (str): The path to the file containing the custom headers. - + Returns: dict: A dictionary containing the custom header data loaded from the file. """ - with open(custom_header_path, 'r', encoding='utf-8') as f: + with open(custom_header_path, "r", encoding="utf-8") as f: return json.load(f) -# pylint: disable=too-many-locals + def custom_header_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - custom_header_path: str + custom_header_path: str, ) -> None: """ Process that monitors the custom header file and checks if received entries exist in it. - + Args: queue (multiprocessing.Queue): A queue to receive header-like entries to check. result_queue (multiprocessing.Queue): A queue to send back True/False depending on match. custom_header_path (str): Path to the file containing custom header entries. """ manager = multiprocessing.Manager() - custom_header_data = manager.dict( - load_custom_header(custom_header_path) - ) + custom_header_data = manager.dict(load_custom_header(custom_header_path)) error_event = threading.Event() diff --git a/pyproxy/modules/filter.py b/pyproxy/modules/filter.py index e801a3b..5cc7564 100644 --- a/pyproxy/modules/filter.py +++ b/pyproxy/modules/filter.py @@ -2,7 +2,7 @@ pyproxy.modules.filter.py This module contains functions and a process to filter and block domains and URLs. -It loads blocked domain names and URLs from specified files, then listens for +It loads blocked domain names and URLs from specified files, then listens for incoming requests to check if the domain or URL should be blocked. Functions: @@ -17,15 +17,18 @@ from urllib.parse import urlparse import requests -def load_blacklist(blocked_sites_path: str, blocked_url_path: str, filter_mode: str) -> set: + +def load_blacklist( + blocked_sites_path: str, blocked_url_path: str, filter_mode: str +) -> set: """ Loads blocked FQDNs or URLs from a file or URL into a set for fast lookup. - + Args: blocked_sites_path (str): The path or URL to the file containing blocked FQDNs. blocked_url_path (str): The path or URL to the file containing blocked URLs. filter_mode (str): Mode to determine if we load from local file or HTTP URL. - + Returns: set: A set of blocked domains/URLs. """ @@ -34,7 +37,7 @@ def load_blacklist(blocked_sites_path: str, blocked_url_path: str, filter_mode: def load_from_file(file_path: str) -> set: data = set() - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, "r", encoding="utf-8") as f: for line in f: data.add(line.strip()) return data @@ -47,7 +50,9 @@ def load_from_http(url: str) -> set: for line in response.text.splitlines(): data.add(line.strip()) except requests.exceptions.RequestException as e: - raise requests.exceptions.RequestException(f"Failed to load data from {url}: {e}") + raise requests.exceptions.RequestException( + f"Failed to load data from {url}: {e}" + ) return data if filter_mode == "local": @@ -59,18 +64,18 @@ def load_from_http(url: str) -> set: return blocked_sites, blocked_url -# pylint: disable=too-many-locals + def filter_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, filter_mode: str, blocked_sites_path: str, blocked_url_path: str, - refresh_interval=5 + refresh_interval=5, ) -> None: """ Process that listens for requests and checks if the domain/URL should be blocked. - + Args: queue (multiprocessing.Queue): A queue to receive URL/domain for checking. result_queue (multiprocessing.Queue): A queue to send back the result of @@ -81,10 +86,16 @@ def filter_process( refresh_interval (int): Interval in seconds to reload the blacklist files. """ manager = multiprocessing.Manager() - blocked_data = manager.dict({ - "sites": load_blacklist(blocked_sites_path, blocked_url_path, filter_mode)[0], - "urls": load_blacklist(blocked_sites_path, blocked_url_path, filter_mode)[1], - }) + blocked_data = manager.dict( + { + "sites": load_blacklist(blocked_sites_path, blocked_url_path, filter_mode)[ + 0 + ], + "urls": load_blacklist(blocked_sites_path, blocked_url_path, filter_mode)[ + 1 + ], + } + ) error_event = threading.Event() @@ -92,9 +103,7 @@ def file_monitor() -> None: try: while True: new_blocked_sites, new_blocked_url = load_blacklist( - blocked_sites_path, - blocked_url_path, - filter_mode + blocked_sites_path, blocked_url_path, filter_mode ) blocked_data["sites"] = new_blocked_sites @@ -131,7 +140,9 @@ def file_monitor() -> None: for blocked_host in blocked_data["sites"] ): result_queue.put((server_host, "Blocked")) - elif any(full_url.startswith(blocked_url) for blocked_url in blocked_data["urls"]): + elif any( + full_url.startswith(blocked_url) for blocked_url in blocked_data["urls"] + ): result_queue.put((full_url, "Blocked")) else: result_queue.put((server_host, "Allowed")) diff --git a/pyproxy/modules/shortcuts.py b/pyproxy/modules/shortcuts.py index 7909be2..f917804 100644 --- a/pyproxy/modules/shortcuts.py +++ b/pyproxy/modules/shortcuts.py @@ -15,19 +15,20 @@ import sys import threading + def load_shortcuts(shortcuts_path: str) -> dict: """ Loads URL alias mappings from a file into a dictionary for fast lookup. - + Args: shortcuts_path (str): The path to the file containing alias=URL mappings. - + Returns: dict: A dictionary mapping aliases to URLs. """ shortcuts = {} - with open(shortcuts_path, 'r', encoding='utf-8') as f: + with open(shortcuts_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if "=" in line: @@ -36,24 +37,22 @@ def load_shortcuts(shortcuts_path: str) -> dict: return shortcuts -# pylint: disable=too-many-locals + def shortcuts_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - shortcuts_path: str + shortcuts_path: str, ) -> None: """ Process that listens for alias requests and resolves them to URLs. - + Args: queue (multiprocessing.Queue): A queue to receive alias for URL resolution. result_queue (multiprocessing.Queue): A queue to send back the resolved URL. shortcuts_path (str): The path to the file containing alias=URL mappings. """ manager = multiprocessing.Manager() - shortcuts_data = manager.dict({ - "shortcuts": load_shortcuts(shortcuts_path) - }) + shortcuts_data = manager.dict({"shortcuts": load_shortcuts(shortcuts_path)}) error_event = threading.Event() diff --git a/pyproxy/monitoring/__init__.py b/pyproxy/monitoring/__init__.py index f0cda04..e69de29 100644 --- a/pyproxy/monitoring/__init__.py +++ b/pyproxy/monitoring/__init__.py @@ -1,7 +0,0 @@ -""" -It contains the initialization code for the Proxy Monitoring system, including -the setup and start of the Flask web server that provides monitoring and status -information for the proxy server. -""" - -from .web import start_flask_server diff --git a/pyproxy/monitoring/web.py b/pyproxy/monitoring/web.py index 41fb072..07af6ba 100644 --- a/pyproxy/monitoring/web.py +++ b/pyproxy/monitoring/web.py @@ -18,7 +18,8 @@ from werkzeug.security import check_password_hash, generate_password_hash import psutil -def start_flask_server(proxy_server: 'ProxyServer', flask_port, flask_pass, debug) -> None: + +def start_flask_server(proxy_server, flask_port, flask_pass, debug) -> None: """ Starts the Flask server for monitoring the ProxyServer. It creates and runs an HTTP server that exposes the proxy server's status, including @@ -40,10 +41,13 @@ class ProxyMonitor: Args: proxy_server (ProxyServer): The ProxyServer instance to monitor. """ + def __init__(self, proxy_server): self.proxy_server = proxy_server - def get_process_info(self) -> Dict[str, Union[int, str, List[Dict[str, Union[int, str]]]]]: + def get_process_info( + self, + ) -> Dict[str, Union[int, str, List[Dict[str, Union[int, str]]]]]: """ Retrieves overall process information for the ProxyServer, including the PID, name, status, and details about threads, @@ -53,19 +57,18 @@ def get_process_info(self) -> Dict[str, Union[int, str, List[Dict[str, Union[int dict: A dictionary containing the process information. """ process_info = { - 'pid': os.getpid(), - 'name': 'ProxyServer', - 'status': 'running', - 'start_time': datetime.fromtimestamp( + "pid": os.getpid(), + "name": "ProxyServer", + "status": "running", + "start_time": datetime.fromtimestamp( psutil.Process(os.getpid()).create_time() - ).strftime('%Y-%m-%d %H:%M:%S'), - 'threads': self.get_threads_info(), - 'subprocesses': self.get_subprocesses_info(), - 'active_connections': self.get_active_connections() + ).strftime("%Y-%m-%d %H:%M:%S"), + "threads": self.get_threads_info(), + "subprocesses": self.get_subprocesses_info(), + "active_connections": self.get_active_connections(), } return process_info - def get_threads_info(self) -> List[Dict[str, Union[int, str]]]: """ Retrieves information about the threads running in the ProxyServer. @@ -76,14 +79,15 @@ def get_threads_info(self) -> List[Dict[str, Union[int, str]]]: """ threads_info = [] for thread in threading.enumerate(): - threads_info.append({ - 'thread_id': thread.ident, - 'name': thread.name, - 'status': self.get_thread_status(thread) - }) + threads_info.append( + { + "thread_id": thread.ident, + "name": thread.name, + "status": self.get_thread_status(thread), + } + ) return threads_info - def get_thread_status(self, thread: threading.Thread) -> str: """ Gets the status of a given thread. @@ -96,15 +100,14 @@ def get_thread_status(self, thread: threading.Thread) -> str: """ try: if thread.is_alive(): - return 'running' - return 'terminated' + return "running" + return "terminated" except AttributeError: - return 'unknown' + return "unknown" - def get_subprocesses_info(self) -> Dict[ - str, - Dict[str, Union[str, List[Dict[str, Union[int, str]]]]] - ]: + def get_subprocesses_info( + self, + ) -> Dict[str, Dict[str, Union[str, List[Dict[str, Union[int, str]]]]]]: """ Retrieves the status of the ProxyServer's subprocesses, including filtering, shortcuts, cancel inspection, and custom header processes. @@ -115,10 +118,10 @@ def get_subprocesses_info(self) -> Dict[ subprocesses_info = {} subprocesses = { - 'filter': self.proxy_server.filter_proc, - 'shortcuts': self.proxy_server.shortcuts_proc, - 'cancel_inspect': self.proxy_server.cancel_inspect_proc, - 'custom_header': self.proxy_server.custom_header_proc + "filter": self.proxy_server.filter_proc, + "shortcuts": self.proxy_server.shortcuts_proc, + "cancel_inspect": self.proxy_server.cancel_inspect_proc, + "custom_header": self.proxy_server.custom_header_proc, } for name, process in subprocesses.items(): @@ -127,9 +130,7 @@ def get_subprocesses_info(self) -> Dict[ return subprocesses_info def get_subprocess_status( - self, - process: multiprocessing.Process, - name: str + self, process: multiprocessing.Process, name: str ) -> Dict[str, Union[str, None, List[Dict[str, Union[int, str]]]]]: """ Retrieves the status of a subprocess. @@ -142,23 +143,22 @@ def get_subprocess_status( dict: A dictionary containing the subprocess status. """ if process is None: - return {'status': 'not started', 'name': name, 'threads': []} + return {"status": "not started", "name": name, "threads": []} try: - status = 'running' if process.is_alive() else 'terminated' + status = "running" if process.is_alive() else "terminated" threads_info = self.get_subprocess_threads_info(process) except AttributeError: - status = 'terminated' + status = "terminated" threads_info = [] return { - 'pid': process.pid if hasattr(process, 'pid') else None, - 'status': status, - 'name': name, - 'threads': threads_info + "pid": process.pid if hasattr(process, "pid") else None, + "status": status, + "name": name, + "threads": threads_info, } def get_subprocess_threads_info( - self, - process: multiprocessing.Process + self, process: multiprocessing.Process ) -> List[Dict[str, Union[int, str]]]: """ Retrieves the threads associated with a subprocess. @@ -172,11 +172,13 @@ def get_subprocess_threads_info( threads_info = [] try: for proc_thread in psutil.Process(process.pid).threads(): - threads_info.append({ - 'thread_id': proc_thread.id, - 'name': f"Thread-{proc_thread.id}", - 'status': self.get_thread_status_by_pid(proc_thread.id) - }) + threads_info.append( + { + "thread_id": proc_thread.id, + "name": f"Thread-{proc_thread.id}", + "status": self.get_thread_status_by_pid(proc_thread.id), + } + ) except (psutil.NoSuchProcess, psutil.AccessDenied): pass return threads_info @@ -207,18 +209,13 @@ def get_active_connections(self) -> List[Dict[str, Union[int, Dict]]]: list: A list of dictionaries containing information about active connections. """ return [ - { - 'thread_id': thread_id, - **conn - } + {"thread_id": thread_id, **conn} for thread_id, conn in self.proxy_server.active_connections.items() ] auth = HTTPBasicAuth() - users = { - "admin": generate_password_hash(flask_pass) - } + users = {"admin": generate_password_hash(flask_pass)} @auth.verify_password def verify_password(username, password): @@ -226,12 +223,12 @@ def verify_password(username, password): return username return None - app = Flask(__name__, static_folder='static') + app = Flask(__name__, static_folder="static") if not debug: - log = logging.getLogger('werkzeug') + log = logging.getLogger("werkzeug") log.setLevel(logging.ERROR) - @app.route('/') + @app.route("/") @auth.login_required def index(): """ @@ -240,9 +237,9 @@ def index(): Returns: str: The rendered HTML content of the index page. """ - return render_template('index.html') + return render_template("index.html") - @app.route('/monitoring', methods=['GET']) + @app.route("/monitoring", methods=["GET"]) @auth.login_required def monitoring(): """ @@ -254,28 +251,29 @@ def monitoring(): monitor = ProxyMonitor(proxy_server) return jsonify(monitor.get_process_info()) - @app.route('/config', methods=['GET']) + @app.route("/config", methods=["GET"]) @auth.login_required def config(): config_data = { - 'host': proxy_server.host_port[0], - 'port': proxy_server.host_port[1], - 'debug': proxy_server.debug, - 'html_403': proxy_server.html_403, - 'logger_config': ( + "host": proxy_server.host_port[0], + "port": proxy_server.host_port[1], + "debug": proxy_server.debug, + "html_403": proxy_server.html_403, + "logger_config": ( proxy_server.logger_config.to_dict() - if proxy_server.logger_config else None + if proxy_server.logger_config + else None ), - 'filter_config': ( + "filter_config": ( proxy_server.filter_config.to_dict() - if proxy_server.filter_config else None + if proxy_server.filter_config + else None ), - 'ssl_config': ( - proxy_server.ssl_config.to_dict() - if proxy_server.ssl_config else None + "ssl_config": ( + proxy_server.ssl_config.to_dict() if proxy_server.ssl_config else None ), - 'flask_port': proxy_server.flask_port + "flask_port": proxy_server.flask_port, } return jsonify(config_data) - app.run(host='0.0.0.0', port=flask_port) # nosec + app.run(host="0.0.0.0", port=flask_port) # nosec diff --git a/pyproxy/server.py b/pyproxy/server.py index 928906e..d4a39ef 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -20,14 +20,15 @@ from pyproxy.handlers.client import ProxyHandlers from pyproxy.modules.filter import filter_process from pyproxy.modules.cancel_inspect import cancel_inspect_process + if not __slim__: from pyproxy.modules.shortcuts import shortcuts_process if not __slim__: from pyproxy.modules.custom_header import custom_header_process if not __slim__: - from pyproxy.monitoring import start_flask_server + from pyproxy.monitoring.web import start_flask_server + -# pylint: disable=too-few-public-methods,too-many-locals class ProxyServer: """ A proxy server that forwards HTTP and HTTPS requests, blocks based on rules, @@ -35,18 +36,43 @@ class ProxyServer: """ _EXCLUDE_DEBUG_KEYS = { - "filter_proc", "filter_queue", "filter_result_queue", - "shortcuts_proc", "shortcuts_queue", "shortcuts_result_queue", - "cancel_inspect_proc", "cancel_inspect_queue", "cancel_inspect_result_queue", - "custom_header_proc", "custom_header_queue", "custom_header_result_queue", - "console_logger", "access_logger", "block_logger", "authorized_ips", - "active_connections" + "filter_proc", + "filter_queue", + "filter_result_queue", + "shortcuts_proc", + "shortcuts_queue", + "shortcuts_result_queue", + "cancel_inspect_proc", + "cancel_inspect_queue", + "cancel_inspect_result_queue", + "custom_header_proc", + "custom_header_queue", + "custom_header_result_queue", + "console_logger", + "access_logger", + "block_logger", + "authorized_ips", + "active_connections", } - def __init__(self, host, port, debug, logger_config, filter_config, - html_403, ssl_config, shortcuts, custom_header, - flask_port, flask_pass, proxy_enable, proxy_host, proxy_port, - authorized_ips): + def __init__( + self, + host, + port, + debug, + logger_config, + filter_config, + html_403, + ssl_config, + shortcuts, + custom_header, + flask_port, + flask_pass, + proxy_enable, + proxy_host, + proxy_port, + authorized_ips, + ): """ Initialize the ProxyServer with configuration parameters. """ @@ -114,46 +140,54 @@ def _initialize_processes(self): self.filter_result_queue, self.filter_config.filter_mode, self.filter_config.blocked_sites, - self.filter_config.blocked_url - ) + self.filter_config.blocked_url, + ), ) self.filter_proc.start() self.console_logger.debug("[*] Starting the filter process...") - # pylint: disable=E0606 - if not __slim__ and self.config_shortcuts and os.path.isfile(self.config_shortcuts): + if ( + not __slim__ + and self.config_shortcuts + and os.path.isfile(self.config_shortcuts) + ): self.shortcuts_proc = multiprocessing.Process( target=shortcuts_process, args=( self.shortcuts_queue, self.shortcuts_result_queue, - self.config_shortcuts - ) + self.config_shortcuts, + ), ) self.shortcuts_proc.start() self.console_logger.debug("[*] Starting the shortcuts process...") - if self.ssl_config.cancel_inspect and os.path.isfile(self.ssl_config.cancel_inspect): + if self.ssl_config.cancel_inspect and os.path.isfile( + self.ssl_config.cancel_inspect + ): self.cancel_inspect_proc = multiprocessing.Process( target=cancel_inspect_process, args=( self.cancel_inspect_queue, self.cancel_inspect_result_queue, - self.ssl_config.cancel_inspect - ) + self.ssl_config.cancel_inspect, + ), ) self.cancel_inspect_proc.start() self.console_logger.debug("[*] Starting the cancel inspection process...") - # pylint: disable=E0606 - if not __slim__ and self.config_custom_header and os.path.isfile(self.config_custom_header): + if ( + not __slim__ + and self.config_custom_header + and os.path.isfile(self.config_custom_header) + ): self.custom_header_proc = multiprocessing.Process( target=custom_header_process, args=( self.custom_header_queue, self.custom_header_result_queue, - self.config_custom_header - ) + self.config_custom_header, + ), ) self.custom_header_proc.start() self.console_logger.debug("[*] Starting the custom header process...") @@ -180,16 +214,18 @@ def _load_authorized_ips(self): with open(self.authorized_ips, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] try: - self.allowed_subnets = [ipaddress.ip_network(line, strict=False) for line in lines] + self.allowed_subnets = [ + ipaddress.ip_network(line, strict=False) for line in lines + ] self.console_logger.debug( - "[*] Loaded %d authorized IPs/subnets", - len(self.allowed_subnets) + "[*] Loaded %d authorized IPs/subnets", len(self.allowed_subnets) ) except ValueError as e: - self.console_logger.error("[*] Invalid IP/subnet in %s: %s", self.authorized_ips, e) + self.console_logger.error( + "[*] Invalid IP/subnet in %s: %s", self.authorized_ips, e + ) self.allowed_subnets = None - # pylint: disable=R0912 def start(self): """ Start the proxy server and listen for incoming client connections. @@ -204,16 +240,14 @@ def start(self): self.console_logger.debug("[*] %s = %s", key, getattr(self, key)) if self.ssl_config.ssl_inspect: - if ( - not self.ssl_config.inspect_ca_cert or - not os.path.isfile(self.ssl_config.inspect_ca_cert) + if not self.ssl_config.inspect_ca_cert or not os.path.isfile( + self.ssl_config.inspect_ca_cert ): raise FileNotFoundError( f"CA certificate not found: {self.ssl_config.inspect_ca_cert}" ) - if ( - not self.ssl_config.inspect_ca_key or - not os.path.isfile(self.ssl_config.inspect_ca_key) + if not self.ssl_config.inspect_ca_key or not os.path.isfile( + self.ssl_config.inspect_ca_key ): raise FileNotFoundError( f"CA key not found: {self.ssl_config.inspect_ca_key}" @@ -222,9 +256,12 @@ def start(self): self._clean_inspection_folder() if self.filter_config.filter_mode == "local": - for file in [self.filter_config.blocked_sites, self.filter_config.blocked_url]: + for file in [ + self.filter_config.blocked_sites, + self.filter_config.blocked_url, + ]: if not os.path.exists(file): - with open(file, "w", encoding='utf-8'): + with open(file, "w", encoding="utf-8"): pass self._initialize_processes() @@ -233,8 +270,8 @@ def start(self): if not __slim__: flask_thread = threading.Thread( target=start_flask_server, - args=(self,self.flask_port,self.flask_pass,self.debug), - daemon=True + args=(self, self.flask_port, self.flask_pass, self.debug), + daemon=True, ) flask_thread.start() self.console_logger.debug("[*] Starting the monitoring process...") @@ -252,7 +289,9 @@ def start(self): if self.allowed_subnets: ip_obj = ipaddress.ip_address(client_ip) if not any(ip_obj in net for net in self.allowed_subnets): - self.console_logger.debug("Unauthorized IP blocked: %s", client_ip) + self.console_logger.debug( + "Unauthorized IP blocked: %s", client_ip + ) client_socket.close() continue @@ -276,22 +315,20 @@ def start(self): proxy_enable=self.proxy_enable, proxy_host=self.proxy_host, proxy_port=self.proxy_port, - active_connections=self.active_connections + active_connections=self.active_connections, ) client_handler = threading.Thread( - target=client.handle_client, - args=(client_socket,), - daemon=True + target=client.handle_client, args=(client_socket,), daemon=True ) client_handler.start() client_ip, client_port = addr self.active_connections[client_handler.ident] = { - 'client_ip': client_ip, - 'client_port': client_port, - 'start_time': time.time(), - 'bytes_sent': 0, - 'bytes_received': 0, - 'thread_name': client_handler.name + "client_ip": client_ip, + "client_port": client_port, + "start_time": time.time(), + "bytes_sent": 0, + "bytes_received": 0, + "thread_name": client_handler.name, } except KeyboardInterrupt: self.console_logger.info("Proxy interrupted, shutting down.") diff --git a/pyproxy/utils/args.py b/pyproxy/utils/args.py index bc34759..0a60790 100644 --- a/pyproxy/utils/args.py +++ b/pyproxy/utils/args.py @@ -10,7 +10,6 @@ from rich_argparse import MetavarTypeRichHelpFormatter from pyproxy.utils.version import __version__ -# pylint: disable=C0301 def parse_args() -> argparse.Namespace: """ @@ -24,38 +23,97 @@ def parse_args() -> argparse.Namespace: """ parser = argparse.ArgumentParser( description="Lightweight and fast python web proxy", - formatter_class=MetavarTypeRichHelpFormatter + formatter_class=MetavarTypeRichHelpFormatter, ) - parser.add_argument("-v", "--version", action='version', version=__version__, help="Show version") + parser.add_argument( + "-v", "--version", action="version", version=__version__, help="Show version" + ) # noqa: E501 parser.add_argument("--debug", action="store_true", help="Enable debug logging") parser.add_argument("-H", "--host", type=str, help="IP address to listen on") parser.add_argument("-P", "--port", type=int, help="Port to listen on") - parser.add_argument("-f", "--config-file", type=str, default="./config.ini", help="Path to config.ini file") + parser.add_argument( + "-f", + "--config-file", + type=str, + default="./config.ini", + help="Path to config.ini file", + ) # noqa: E501 parser.add_argument("--access-log", type=str, help="Path to the access log file") parser.add_argument("--block-log", type=str, help="Path to the block log file") - parser.add_argument("--html-403", type=str, help="Path to the custom 403 Forbidden HTML page") - parser.add_argument("--no-filter", action="store_true", help="Disable URL and domain filtering") - parser.add_argument("--filter-mode", type=str, choices=["local", "http"], help="Filter list mode") - parser.add_argument("--blocked-sites", type=str, help="Path to the text file containing the list of sites to block") - parser.add_argument("--blocked-url", type=str, help="Path to the text file containing the list of URLs to block") - parser.add_argument("--shortcuts", type=str, help="Path to the text file containing the list of shortcuts") - parser.add_argument("--custom-header", type=str, help="Path to the json file containing the list of custom headers") - parser.add_argument("--authorized-ips", type=str, help="Path to the txt file containing the list of authorized ips") - parser.add_argument("--no-logging-access", action="store_true", help="Disable access logging") - parser.add_argument("--no-logging-block", action="store_true", help="Disable block logging") - parser.add_argument("--ssl-inspect", action="store_true", help="Enable SSL inspection") - parser.add_argument("--inspect-ca-cert", type=str, help="Path to the CA certificate") + parser.add_argument( + "--html-403", type=str, help="Path to the custom 403 Forbidden HTML page" + ) + parser.add_argument( + "--no-filter", action="store_true", help="Disable URL and domain filtering" + ) + parser.add_argument( + "--filter-mode", type=str, choices=["local", "http"], help="Filter list mode" + ) # noqa: E501 + parser.add_argument( + "--blocked-sites", + type=str, + help="Path to the text file containing the list of sites to block", + ) # noqa: E501 + parser.add_argument( + "--blocked-url", + type=str, + help="Path to the text file containing the list of URLs to block", + ) # noqa: E501 + parser.add_argument( + "--shortcuts", + type=str, + help="Path to the text file containing the list of shortcuts", + ) # noqa: E501 + parser.add_argument( + "--custom-header", + type=str, + help="Path to the json file containing the list of custom headers", + ) # noqa: E501 + parser.add_argument( + "--authorized-ips", + type=str, + help="Path to the txt file containing the list of authorized ips", + ) # noqa: E501 + parser.add_argument( + "--no-logging-access", action="store_true", help="Disable access logging" + ) + parser.add_argument( + "--no-logging-block", action="store_true", help="Disable block logging" + ) + parser.add_argument( + "--ssl-inspect", action="store_true", help="Enable SSL inspection" + ) + parser.add_argument( + "--inspect-ca-cert", type=str, help="Path to the CA certificate" + ) parser.add_argument("--inspect-ca-key", type=str, help="Path to the CA key") - parser.add_argument("--inspect-certs-folder", type=str, help="Path to the generated certificates folder") - parser.add_argument("--cancel-inspect", type=str, help="Path to the text file containing the list of URLs without ssl inspection") - parser.add_argument("--flask-port", type=int, help="Port to listen on for monitoring interface") - parser.add_argument("--flask-pass", type=int, help="Default password to Flask interface") - parser.add_argument("--proxy-enable", action="store_true", help="Enable proxy after PyProxy") + parser.add_argument( + "--inspect-certs-folder", + type=str, + help="Path to the generated certificates folder", + ) # noqa: E501 + parser.add_argument( + "--cancel-inspect", + type=str, + help="Path to the text file containing the list of URLs without ssl inspection", + ) # noqa: E501 + parser.add_argument( + "--flask-port", type=int, help="Port to listen on for monitoring interface" + ) + parser.add_argument( + "--flask-pass", type=int, help="Default password to Flask interface" + ) + parser.add_argument( + "--proxy-enable", action="store_true", help="Enable proxy after PyProxy" + ) parser.add_argument("--proxy-host", type=str, help="Proxy IP to use after PyProxy") - parser.add_argument("--proxy-port", type=int, help="Proxy Port to use after PyProxy") + parser.add_argument( + "--proxy-port", type=int, help="Proxy Port to use after PyProxy" + ) return parser.parse_args() + def load_config(config_path: str) -> configparser.ConfigParser: """ Loads the configuration file and returns the parsed config object. @@ -70,16 +128,25 @@ def load_config(config_path: str) -> configparser.ConfigParser: config.read(config_path) return config -def get_config_value(args: argparse.Namespace, config: configparser.ConfigParser, arg_name: str, section: str, fallback_value: str) -> str: + +def get_config_value( + args: argparse.Namespace, + config: configparser.ConfigParser, + arg_name: str, + section: str, + fallback_value: str, +) -> str: """ - Retrieves the configuration value, either from the command-line arguments or from the config file. + Retrieves the configuration value, either from the command-line + arguments or from the config file. Args: args (argparse.Namespace): The parsed command-line arguments object. config (configparser.ConfigParser): The parsed configuration object. arg_name (str): The name of the command-line argument. section (str): The section in the config file where the value is located. - fallback_value (str): The fallback value to return if neither argument nor config has a value. + fallback_value (str): The fallback value to return if neither + argument nor config has a value. Returns: str: The final value, either from command-line arguments, config file, or fallback. @@ -95,6 +162,7 @@ def get_config_value(args: argparse.Namespace, config: configparser.ConfigParser return config.get(section, arg_name, fallback=fallback_value) + def str_to_bool(value: str) -> bool: """ Converts a string representation of truth to a boolean value. diff --git a/pyproxy/utils/config.py b/pyproxy/utils/config.py index 63bd2f1..d5dbdfa 100644 --- a/pyproxy/utils/config.py +++ b/pyproxy/utils/config.py @@ -4,11 +4,12 @@ This module defines configuration classes used by the HTTP/HTTPS proxy. """ -# pylint: disable=R0903 + class ProxyConfigLogger: """ Handles logging configuration for the proxy. """ + def __init__(self, access_log, block_log, no_logging_access, no_logging_block): self.access_log = access_log self.block_log = block_log @@ -18,27 +19,30 @@ def __init__(self, access_log, block_log, no_logging_access, no_logging_block): self.no_logging_block = no_logging_block def __repr__(self): - return (f"ProxyConfigLogger(access_log={self.access_log}, " - f"block_log={self.block_log}, " - f"no_logging_access={self.no_logging_access}, " - f"no_logging_block={self.no_logging_block})") - + return ( + f"ProxyConfigLogger(access_log={self.access_log}, " + f"block_log={self.block_log}, " + f"no_logging_access={self.no_logging_access}, " + f"no_logging_block={self.no_logging_block})" + ) def to_dict(self): """ Converts the ProxyConfigLogger instance into a dictionary. """ return { - 'access_log': self.access_log, - 'block_log': self.block_log, - 'no_logging_access': self.no_logging_access, - 'no_logging_block': self.no_logging_block + "access_log": self.access_log, + "block_log": self.block_log, + "no_logging_access": self.no_logging_access, + "no_logging_block": self.no_logging_block, } + class ProxyConfigFilter: """ Manages filtering configuration for the proxy. """ + def __init__(self, no_filter, filter_mode, blocked_sites, blocked_url): self.no_filter = no_filter self.filter_mode = filter_mode @@ -46,28 +50,38 @@ def __init__(self, no_filter, filter_mode, blocked_sites, blocked_url): self.blocked_url = blocked_url def __repr__(self): - return (f"ProxyConfigFilter(no_filter={self.no_filter}, " - f"filter_mode='{self.filter_mode}', " - f"blocked_sites={self.blocked_sites}, " - f"blocked_url={self.blocked_url})") + return ( + f"ProxyConfigFilter(no_filter={self.no_filter}, " + f"filter_mode='{self.filter_mode}', " + f"blocked_sites={self.blocked_sites}, " + f"blocked_url={self.blocked_url})" + ) def to_dict(self): """ Converts the ProxyConfigFilter instance into a dictionary. """ return { - 'no_filter': self.no_filter, - 'filter_mode': self.filter_mode, - 'blocked_sites': self.blocked_sites, - 'blocked_url': self.blocked_url + "no_filter": self.no_filter, + "filter_mode": self.filter_mode, + "blocked_sites": self.blocked_sites, + "blocked_url": self.blocked_url, } + class ProxyConfigSSL: """ Handles SSL/TLS inspection configuration. """ - def __init__(self, ssl_inspect, inspect_ca_cert, inspect_ca_key, - inspect_certs_folder, cancel_inspect): + + def __init__( + self, + ssl_inspect, + inspect_ca_cert, + inspect_ca_key, + inspect_certs_folder, + cancel_inspect, + ): self.ssl_inspect = ssl_inspect self.inspect_ca_cert = inspect_ca_cert self.inspect_ca_key = inspect_ca_key @@ -75,20 +89,22 @@ def __init__(self, ssl_inspect, inspect_ca_cert, inspect_ca_key, self.cancel_inspect = cancel_inspect def __repr__(self): - return (f"ProxyConfigSSL(ssl_inspect={self.ssl_inspect}, " - f"inspect_ca_cert='{self.inspect_ca_cert}', " - f"inspect_ca_key='{self.inspect_ca_key}', " - f"inspect_certs_folder='{self.inspect_certs_folder}', " - f"cancel_inspect={self.cancel_inspect})") + return ( + f"ProxyConfigSSL(ssl_inspect={self.ssl_inspect}, " + f"inspect_ca_cert='{self.inspect_ca_cert}', " + f"inspect_ca_key='{self.inspect_ca_key}', " + f"inspect_certs_folder='{self.inspect_certs_folder}', " + f"cancel_inspect={self.cancel_inspect})" + ) def to_dict(self): """ Converts the ProxyConfigSSL instance into a dictionary. """ return { - 'ssl_inspect': self.ssl_inspect, - 'inspect_ca_cert': self.inspect_ca_cert, - 'inspect_ca_key': self.inspect_ca_key, - 'inspect_certs_folder': self.inspect_certs_folder, - 'cancel_inspect': self.cancel_inspect + "ssl_inspect": self.ssl_inspect, + "inspect_ca_cert": self.inspect_ca_cert, + "inspect_ca_key": self.inspect_ca_key, + "inspect_certs_folder": self.inspect_certs_folder, + "cancel_inspect": self.cancel_inspect, } diff --git a/pyproxy/utils/crypto.py b/pyproxy/utils/crypto.py index bf35673..62bf9ab 100644 --- a/pyproxy/utils/crypto.py +++ b/pyproxy/utils/crypto.py @@ -7,6 +7,7 @@ import os from OpenSSL import crypto + def generate_certificate(domain, certs_folder, ca_cert, ca_key): """ Generates a self-signed SSL certificate for the given domain. @@ -24,24 +25,24 @@ def generate_certificate(domain, certs_folder, ca_cert, ca_key): key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, 2048) - with open(ca_cert, "r", encoding='utf-8') as f: + with open(ca_cert, "r", encoding="utf-8") as f: ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) - with open(ca_key, "r", encoding='utf-8') as f: + with open(ca_key, "r", encoding="utf-8") as f: ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) cert = crypto.X509() - cert.set_serial_number(int.from_bytes(os.urandom(16), 'big')) + cert.set_serial_number(int.from_bytes(os.urandom(16), "big")) cert.get_subject().CN = domain cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(365 * 24 * 60 * 60) cert.set_issuer(ca_cert.get_subject()) cert.set_pubkey(key) san = f"DNS:{domain}" - cert.add_extensions([ - crypto.X509Extension(b"subjectAltName", False, san.encode()) - ]) + cert.add_extensions( + [crypto.X509Extension(b"subjectAltName", False, san.encode())] + ) - cert.sign(ca_key, 'sha256') + cert.sign(ca_key, "sha256") with open(cert_path, "wb") as f: f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) diff --git a/pyproxy/utils/http_req.py b/pyproxy/utils/http_req.py index 40708c3..4601763 100644 --- a/pyproxy/utils/http_req.py +++ b/pyproxy/utils/http_req.py @@ -4,6 +4,7 @@ HTTP request parsing utilities for pyproxy. """ + def extract_headers(request_str): """ Extracts the HTTP headers from a raw HTTP request string. @@ -22,28 +23,31 @@ def extract_headers(request_str): headers[key.strip()] = value.strip() return headers + def parse_url(url): """ Parses the URL to extract the host and port for connecting to the target server. - + Args: url (str): The URL to be parsed. - + Returns: tuple: The server host and port. """ http_pos = url.find("//") if http_pos != -1: - url = url[(http_pos + 2):] + url = url[(http_pos + 2) :] port_pos = url.find(":") path_pos = url.find("/") if path_pos == -1: path_pos = len(url) - server_host = url[:path_pos] if port_pos == -1 or port_pos > path_pos else url[:port_pos] + server_host = ( + url[:path_pos] if port_pos == -1 or port_pos > path_pos else url[:port_pos] + ) if port_pos == -1 or port_pos > path_pos: server_port = 80 else: - server_port = int(url[(port_pos + 1):path_pos]) + server_port = int(url[(port_pos + 1) : path_pos]) return server_host, server_port diff --git a/pyproxy/utils/logger.py b/pyproxy/utils/logger.py index 969ade5..7909fa2 100644 --- a/pyproxy/utils/logger.py +++ b/pyproxy/utils/logger.py @@ -7,6 +7,7 @@ import logging import os + def configure_console_logger() -> logging.Logger: """ Configures and returns a logger that outputs log messages to the console. @@ -16,20 +17,23 @@ def configure_console_logger() -> logging.Logger: """ console_logger = logging.getLogger("ConsoleLogger") console_logger.setLevel(logging.INFO) - console_formatter = logging.Formatter('%(asctime)s - %(message)s', datefmt='%d/%m/%Y %H:%M:%S') + console_formatter = logging.Formatter( + "%(asctime)s - %(message)s", datefmt="%d/%m/%Y %H:%M:%S" + ) console_handler = logging.StreamHandler() console_handler.setFormatter(console_formatter) console_logger.addHandler(console_handler) return console_logger + def configure_file_logger(log_path: str, name: str) -> logging.Logger: """ Configures and returns a logger that writes log messages to a specified file. - + Args: log_path (str): The path where the log file will be created or appended to. name (str): Logger's name. - + Returns: logging.Logger: A logger instance that writes to the specified log file. """ @@ -37,6 +41,6 @@ def configure_file_logger(log_path: str, name: str) -> logging.Logger: file_logger = logging.getLogger(name) file_logger.setLevel(logging.INFO) file_handler = logging.FileHandler(log_path) - file_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s')) + file_handler.setFormatter(logging.Formatter("%(asctime)s - %(message)s")) file_logger.addHandler(file_handler) return file_logger diff --git a/tests/modules/test_cancel_inspect.py b/tests/modules/test_cancel_inspect.py index 69d0356..3709f8a 100644 --- a/tests/modules/test_cancel_inspect.py +++ b/tests/modules/test_cancel_inspect.py @@ -21,10 +21,9 @@ class TestCancelInspect(unittest.TestCase): """Unit tests for the cancel_inspection.py module.""" - # pylint: disable=R1732 def setUp(self): """Set up a temporary file with test data for cancel inspection.""" - self.temp_file = tempfile.NamedTemporaryFile(mode='w+', delete=False) + self.temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) self.temp_file.write("http://example.com/1\nhttp://example.com/2\n") self.temp_file.close() self.path = self.temp_file.name @@ -46,8 +45,7 @@ def test_cancel_inspect_process(self): result_queue = multiprocessing.Queue() process = multiprocessing.Process( - target=cancel_inspect_process, - args=(queue, result_queue, self.path) + target=cancel_inspect_process, args=(queue, result_queue, self.path) ) process.start() @@ -65,5 +63,5 @@ def test_cancel_inspect_process(self): process.join() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/modules/test_custom_header.py b/tests/modules/test_custom_header.py index 36b0a32..27a413e 100644 --- a/tests/modules/test_custom_header.py +++ b/tests/modules/test_custom_header.py @@ -21,18 +21,12 @@ class TestCustomHeader(unittest.TestCase): """Unit tests for the custom_header.py module.""" - # pylint: disable=R1732 def setUp(self): """Set up a temporary JSON file with test data for custom headers.""" - self.temp_file = tempfile.NamedTemporaryFile(mode='w+', delete=False) + self.temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) self.sample_data = { - "http://example.com": { - "X-Test-Header": "123", - "X-Another": "456" - }, - "http://another.com": { - "X-Custom": "abc" - } + "http://example.com": {"X-Test-Header": "123", "X-Another": "456"}, + "http://another.com": {"X-Custom": "abc"}, } json.dump(self.sample_data, self.temp_file) self.temp_file.close() @@ -54,8 +48,7 @@ def test_custom_header_process(self): result_queue = multiprocessing.Queue() process = multiprocessing.Process( - target=custom_header_process, - args=(queue, result_queue, self.path) + target=custom_header_process, args=(queue, result_queue, self.path) ) process.start() @@ -63,10 +56,7 @@ def test_custom_header_process(self): queue.put("http://example.com") result = result_queue.get(timeout=3) - self.assertEqual(result, { - "X-Test-Header": "123", - "X-Another": "456" - }) + self.assertEqual(result, {"X-Test-Header": "123", "X-Another": "456"}) queue.put("http://nonexistent.com") result = result_queue.get(timeout=3) @@ -76,5 +66,5 @@ def test_custom_header_process(self): process.join() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/modules/test_filter.py b/tests/modules/test_filter.py index c8b53f0..1e75f47 100644 --- a/tests/modules/test_filter.py +++ b/tests/modules/test_filter.py @@ -26,6 +26,7 @@ import requests from pyproxy.modules.filter import load_blacklist, filter_process + class TestFilter(unittest.TestCase): """ Test suite for the filter module. @@ -48,7 +49,7 @@ def test_load_blacklist(self): with patch( "builtins.open", new_callable=mock_open, - read_data="blocked.com\nallowed.com/blocked" + read_data="blocked.com\nallowed.com/blocked", ): blocked_sites, blocked_urls = load_blacklist( "blocked_sites.txt", "blocked_urls.txt", "local" @@ -64,23 +65,24 @@ def test_load_blacklist_file_not_found(self, _mock_file): with self.assertRaises(FileNotFoundError): load_blacklist("invalid_file.txt", "blocked_urls.txt", "local") - @patch("requests.get", side_effect=requests.exceptions.RequestException("Failed to load")) + @patch( + "requests.get", + side_effect=requests.exceptions.RequestException("Failed to load"), + ) def test_load_blacklist_http_error(self, _mock_request): """Tests that an HTTP error is handled correctly when loading blacklists.""" with self.assertRaises(requests.exceptions.RequestException): load_blacklist( "http://example.com/blocked_sites", "http://example.com/blocked_urls", - "http" + "http", ) @patch("builtins.open", new_callable=mock_open, read_data="") def test_load_blacklist_empty_file(self, _mock_file): """Tests that an empty file returns empty sets for blocked sites and URLs.""" blocked_sites, blocked_urls = load_blacklist( - "empty_sites.txt", - "empty_urls.txt", - "local" + "empty_sites.txt", "empty_urls.txt", "local" ) self.assertEqual(len(blocked_sites), 0) self.assertEqual(len(blocked_urls), 0) @@ -89,7 +91,7 @@ def _test_filter_process_helper( self, input_urls, expected_results, - patch_data="blocked.com\nallowed.com/blocked" + patch_data="blocked.com\nallowed.com/blocked", ): """Helper method to test filter_process with different inputs.""" with patch("builtins.open", new_callable=mock_open, read_data=patch_data): @@ -100,8 +102,8 @@ def _test_filter_process_helper( self.result_queue, "local", "blocked_sites.txt", - "blocked_urls.txt" - ) + "blocked_urls.txt", + ), ) process.start() @@ -122,13 +124,13 @@ def test_filter_process(self): "http://blocked.com/", "http://allowed.com/", "http://allowed.com/blocked", - "http://allowed.com/allowed" + "http://allowed.com/allowed", ] expected_results = [ ("blocked.com", "Blocked"), ("allowed.com", "Allowed"), ("allowed.com/blocked", "Blocked"), - ("allowed.com", "Allowed") + ("allowed.com", "Allowed"), ] self._test_filter_process_helper(input_urls, expected_results) @@ -137,17 +139,15 @@ def test_filter_process_with_query_string(self): input_urls = [ "http://blocked.com?tracking=123", "http://example.com/secret?auth=false", - "http://safe.com/page?debug=true" + "http://safe.com/page?debug=true", ] expected_results = [ ("blocked.com", "Blocked"), ("example.com/secret", "Blocked"), - ("safe.com", "Allowed") + ("safe.com", "Allowed"), ] self._test_filter_process_helper( - input_urls, - expected_results, - patch_data="blocked.com\nexample.com/secret" + input_urls, expected_results, patch_data="blocked.com\nexample.com/secret" ) def test_filter_process_subdomain_not_blocked(self): @@ -156,29 +156,30 @@ def test_filter_process_subdomain_not_blocked(self): """ input_urls = ["http://sub.blocked.com/"] expected_results = [("sub.blocked.com", "Allowed")] - self._test_filter_process_helper(input_urls, expected_results, patch_data="blocked.com\n") + self._test_filter_process_helper( + input_urls, expected_results, patch_data="blocked.com\n" + ) def test_filter_process_special_characters(self): """Tests if URLs with special characters are correctly handled.""" input_urls = ["http://weird-site.com/"] expected_results = [("weird-site.com", "Blocked")] self._test_filter_process_helper( - input_urls, - expected_results, - patch_data="weird-site.com\n" + input_urls, expected_results, patch_data="weird-site.com\n" ) def test_filter_process_with_path_and_port(self): """Tests if URLs with paths and ports are correctly filtered.""" input_urls = [ "http://blocked.com:8080/path/to/resource", - "http://allowed.com/blocked/resource" + "http://allowed.com/blocked/resource", ] expected_results = [ ("blocked.com", "Blocked"), - ("allowed.com/blocked/resource", "Blocked") + ("allowed.com/blocked/resource", "Blocked"), ] self._test_filter_process_helper(input_urls, expected_results) + if __name__ == "__main__": unittest.main() diff --git a/tests/modules/test_shortcuts.py b/tests/modules/test_shortcuts.py index 980f513..3438085 100644 --- a/tests/modules/test_shortcuts.py +++ b/tests/modules/test_shortcuts.py @@ -49,7 +49,7 @@ def test_load_shortcuts(self): with patch( "builtins.open", new_callable=mock_open, - read_data="alias1=http://example.com\nalias2=http://test.com" + read_data="alias1=http://example.com\nalias2=http://test.com", ): shortcuts = load_shortcuts("shortcuts.txt") self.assertEqual(shortcuts["alias1"], "http://example.com") @@ -63,16 +63,13 @@ def test_load_shortcuts_file_not_found(self, _mock_file): load_shortcuts("invalid_file.txt") def _test_shortcuts_process_helper( - self, - alias, - expected_url, - patch_data="alias1=http://example.com" + self, alias, expected_url, patch_data="alias1=http://example.com" ): """Helper method to test shortcuts_process with different alias requests.""" with patch("builtins.open", new_callable=mock_open, read_data=patch_data): process = multiprocessing.Process( target=shortcuts_process, - args=(self.queue, self.result_queue, "shortcuts.txt") + args=(self.queue, self.result_queue, "shortcuts.txt"), ) process.start() @@ -97,11 +94,11 @@ def test_shortcuts_process_with_multiple_aliases(self): with patch( "builtins.open", new_callable=mock_open, - read_data="alias1=http://example.com\nalias2=http://test.com" + read_data="alias1=http://example.com\nalias2=http://test.com", ): process = multiprocessing.Process( target=shortcuts_process, - args=(self.queue, self.result_queue, "shortcuts.txt") + args=(self.queue, self.result_queue, "shortcuts.txt"), ) process.start() @@ -117,5 +114,6 @@ def test_shortcuts_process_with_multiple_aliases(self): process.terminate() process.join() + if __name__ == "__main__": unittest.main() diff --git a/tests/utils/test_crypto.py b/tests/utils/test_crypto.py index 9259fb3..5c9efc8 100644 --- a/tests/utils/test_crypto.py +++ b/tests/utils/test_crypto.py @@ -10,6 +10,7 @@ from OpenSSL import crypto from pyproxy.utils.crypto import generate_certificate + class TestCrypto(unittest.TestCase): """ Test suite for the crypto module. @@ -41,7 +42,7 @@ def _generate_fake_ca(self): ca_cert.set_issuer(ca_cert.get_subject()) ca_cert.set_pubkey(ca_key) - ca_cert.sign(ca_key, 'sha256') + ca_cert.sign(ca_key, "sha256") with open(self.ca_cert_path, "wb") as cert_file: cert_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert)) @@ -53,13 +54,10 @@ def test_generate_certificate(self): Test the `generate_certificate` function to ensure it generates a certificate and private key file for a given domain. """ - if not self.certs_folder.endswith('/'): - self.certs_folder += '/' + if not self.certs_folder.endswith("/"): + self.certs_folder += "/" cert_path, key_path = generate_certificate( - self.domain, - self.certs_folder, - self.ca_cert_path, - self.ca_key_path + self.domain, self.certs_folder, self.ca_cert_path, self.ca_key_path ) expected_cert_path = os.path.join(self.certs_folder, f"{self.domain}.pem") @@ -79,13 +77,15 @@ def test_generate_certificate(self): key_data = key_file.read() key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_data) - self.assertEqual(crypto.dump_publickey(crypto.FILETYPE_PEM, cert.get_pubkey()), - crypto.dump_publickey(crypto.FILETYPE_PEM, key)) + self.assertEqual( + crypto.dump_publickey(crypto.FILETYPE_PEM, cert.get_pubkey()), + crypto.dump_publickey(crypto.FILETYPE_PEM, key), + ) def tearDown(self): """ Cleanup method executed after each test. - + - Deletes the generated certificate and key files if they exist. - Removes the fake CA files. """ @@ -105,5 +105,6 @@ def tearDown(self): if os.path.exists(self.certs_folder): os.rmdir(self.certs_folder) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/utils/test_http_req.py b/tests/utils/test_http_req.py index d4fcd6d..b9ef2fc 100644 --- a/tests/utils/test_http_req.py +++ b/tests/utils/test_http_req.py @@ -7,6 +7,7 @@ import unittest from pyproxy.utils.http_req import extract_headers, parse_url + class TestHttpReq(unittest.TestCase): """ Test suite for the HTTP request utilities. @@ -27,7 +28,7 @@ def test_extract_headers(self): expected_headers = { "Host": "example.com", "User-Agent": "Mozilla/5.0", - "Accept": "*/*" + "Accept": "*/*", } headers = extract_headers(request_str) @@ -63,5 +64,6 @@ def test_parse_url(self): self.assertEqual(host, expected_host) self.assertEqual(port, expected_port) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/utils/test_logger.py b/tests/utils/test_logger.py index b55f370..e586eec 100644 --- a/tests/utils/test_logger.py +++ b/tests/utils/test_logger.py @@ -11,12 +11,12 @@ from unittest.mock import patch, MagicMock from pyproxy.utils.logger import configure_console_logger, configure_file_logger + class TestLogger(unittest.TestCase): """ Test suite for the logger module. """ - # pylint: disable=unused-argument @patch("sys.stdout") def test_configure_console_logger(self, mock_stdout): """ @@ -63,5 +63,6 @@ def tearDown(self): if os.path.exists(log_file): os.remove(log_file) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() From a66a9ec561df748a8cc7dacb5e14ae561f14dca5 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:27:23 +0200 Subject: [PATCH 3/8] move pyproxy.py in pyproxy folder & build python packages --- .github/workflows/main.yml | 59 +++++++++++++++++++++++++++++++- Dockerfile | 2 +- Dockerfile.slim | 2 +- README.md | 2 +- benchmark/README.md | 2 +- pyproject.toml | 43 +++++++++++++++++++++++ pyproxy.py => pyproxy/pyproxy.py | 6 ++-- setup.py | 12 +++++++ 8 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 pyproject.toml rename pyproxy.py => pyproxy/pyproxy.py (94%) create mode 100644 setup.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 400cbca..37f5c19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,4 +97,61 @@ jobs: docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }}-slim docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest-slim docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:${{ env.VERSION }} - docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest \ No newline at end of file + docker push ghcr.io/${{ env.REPO_OWNER }}/pyproxy:latest + + build-packages: + needs: unittest + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Get version from version.py + id: get_version + run: | + VERSION=$(python -c "from pyproxy.utils.version import __version__; print(__version__)") + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + + - name: Create Tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag v${{ steps.get_version.outputs.version }} + git push origin v${{ steps.get_version.outputs.version }} + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + with: + tag_name: v${{ steps.get_version.outputs.version }} + release_name: Release v${{ steps.get_version.outputs.version }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install build dependencies + run: pip install --no-cache-dir -U pip . build + + - name: Build package + run: python -m build --sdist --wheel + - name: Upload built distributions + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + - name: Install release dependencies + run: pip install --no-cache-dir -U pip . twine packaging + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/Dockerfile b/Dockerfile index ed8c134..dca7f7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,4 @@ WORKDIR /app COPY --from=builder /usr/local /usr/local COPY --from=builder /app /app EXPOSE 8080 -ENTRYPOINT ["python3", "pyproxy.py"] +ENTRYPOINT ["python3", "-m", "pyproxy.pyproxy"] diff --git a/Dockerfile.slim b/Dockerfile.slim index 5ba2fcf..54f9f59 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -19,4 +19,4 @@ WORKDIR /app COPY --from=builder /usr/local /usr/local COPY --from=builder /app /app EXPOSE 8080 -ENTRYPOINT ["python3", "pyproxy.py"] +ENTRYPOINT ["python3", "-m", "pyproxy.pyproxy"] diff --git a/README.md b/README.md index dcd7d7f..85514a4 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ You can use slim images by adding `-slim` to the end of the tags ### Start the proxy ```bash -python3 pyproxy.py +python3 -m pyproxy.pyproxy ``` The proxy will be available at: `0.0.0.0:8080`. The access log will be available at `./logs/access.log`. diff --git a/benchmark/README.md b/benchmark/README.md index 626f296..836726c 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -19,7 +19,7 @@ pip install -r benchmark/requirements.txt ### Start the Proxy Before running the benchmark, ensure that **pyproxy** is running. Start the proxy by running: ```bash -python3 pyproxy.py +python3 -m pyproxy.pyproxy ``` The proxy will be available at `0.0.0.0:8080`. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a8ae593 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "pyproxyx" +description = "Lightweight and fast python web proxy" +readme = "README.md" +license = "MIT" +license-files = [ + "LICENSE", +] +authors = [{name = "6C656C65"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Internet", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "Typing :: Typed", +] +dynamic = ["version", "dependencies"] + +[project.urls] +Documentation = "https://github.com/6C656C65/pyproxy/wiki" +"Issue tracker" = "https://github.com/6C656C65/pyproxy/issues" + +[tool.setuptools.packages] +find = {} + +[tool.setuptools.dynamic] +dependencies = { file = "requirements.txt" } + +[project.scripts] +pyproxy = "pyproxy.pyproxy:main" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/pyproxy.py b/pyproxy/pyproxy.py similarity index 94% rename from pyproxy.py rename to pyproxy/pyproxy.py index 420b0a1..66a15a8 100644 --- a/pyproxy.py +++ b/pyproxy/pyproxy.py @@ -4,9 +4,9 @@ to those URLs. The proxy can handle both HTTP and HTTPS requests, and logs access and block events. """ -from pyproxy.server import ProxyServer -from pyproxy.utils.args import parse_args, load_config, get_config_value, str_to_bool -from pyproxy.utils.config import ProxyConfigLogger, ProxyConfigFilter, ProxyConfigSSL +from .server import ProxyServer +from .utils.args import parse_args, load_config, get_config_value, str_to_bool +from .utils.config import ProxyConfigLogger, ProxyConfigFilter, ProxyConfigSSL def main(): """ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5d87913 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup +import os + +def get_version(): + base_dir = os.path.dirname(__file__) + version_path = os.path.join(base_dir, "pyproxy", "utils", "version.py") + version_ns = {} + with open(version_path, "r") as f: + exec(f.read(), version_ns) + return version_ns["__version__"] + +setup(version=get_version()) \ No newline at end of file From a1a7b42d0285cd57537ccccabf48c3df9ba2150d Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:35:27 +0200 Subject: [PATCH 4/8] fix flake8 & black scan --- pyproxy/pyproxy.py | 90 +++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 29 deletions(-) diff --git a/pyproxy/pyproxy.py b/pyproxy/pyproxy.py index 66a15a8..bb70f22 100644 --- a/pyproxy/pyproxy.py +++ b/pyproxy/pyproxy.py @@ -1,6 +1,6 @@ """ This script implements a lightweight and fast Python-based proxy server. -It listens for client requests, filters URLs based on a list, and allows or blocks access +It listens for client requests, filters URLs based on a list, and allows or blocks access to those URLs. The proxy can handle both HTTP and HTTPS requests, and logs access and block events. """ @@ -8,47 +8,78 @@ from .utils.args import parse_args, load_config, get_config_value, str_to_bool from .utils.config import ProxyConfigLogger, ProxyConfigFilter, ProxyConfigSSL + def main(): """ - Main entry point of the proxy server. It parses command-line arguments, loads the configuration file, - retrieves configuration values, and starts the proxy server. + Main entry point of the proxy server. It parses command-line arguments, + loads the configuration file, retrieves configuration values, and starts the proxy server. """ args = parse_args() config = load_config(args.config_file) - host = get_config_value(args, config, 'host', 'Server', "0.0.0.0") # nosec - port = int(get_config_value(args, config, 'port', 'Server', 8080)) # nosec - debug = get_config_value(args, config, 'debug', 'Logging', False) - html_403 = get_config_value(args, config, 'html_403', 'Files', "assets/403.html") - shortcuts = get_config_value(args, config, 'shortcuts', 'Options', "config/shortcuts.txt") - custom_header = get_config_value(args, config, 'custom_header', 'Options', "config/custom_header.json") - authorized_ips = get_config_value(args, config, 'authorized_ips', 'Options', "config/authorized_ips.txt") - flask_port = get_config_value(args, config, 'flask_port', 'Monitoring', 5000) - flask_pass = get_config_value(args, config, 'flask_pass', 'Monitoring', "password") - proxy_enable = get_config_value(args, config, 'proxy_enable', 'Proxy', False) - proxy_host = get_config_value(args, config, 'proxy_host', 'Proxy', "127.0.0.1") - proxy_port = get_config_value(args, config, 'proxy_port', 'Proxy', 8081) + host = get_config_value(args, config, "host", "Server", "0.0.0.0") # nosec + port = int(get_config_value(args, config, "port", "Server", 8080)) # nosec + debug = get_config_value(args, config, "debug", "Logging", False) + html_403 = get_config_value(args, config, "html_403", "Files", "assets/403.html") + shortcuts = get_config_value( + args, config, "shortcuts", "Options", "config/shortcuts.txt" + ) + custom_header = get_config_value( + args, config, "custom_header", "Options", "config/custom_header.json" + ) + authorized_ips = get_config_value( + args, config, "authorized_ips", "Options", "config/authorized_ips.txt" + ) + flask_port = get_config_value(args, config, "flask_port", "Monitoring", 5000) + flask_pass = get_config_value(args, config, "flask_pass", "Monitoring", "password") + proxy_enable = get_config_value(args, config, "proxy_enable", "Proxy", False) + proxy_host = get_config_value(args, config, "proxy_host", "Proxy", "127.0.0.1") + proxy_port = get_config_value(args, config, "proxy_port", "Proxy", 8081) logger_config = ProxyConfigLogger( - access_log=get_config_value(args, config, 'access_log', 'Logging', "logs/access.log"), - block_log=get_config_value(args, config, 'block_log', 'Logging', "logs/block.log"), - no_logging_access=str_to_bool(get_config_value(args, config, 'no_logging_access', 'Logging', False)), - no_logging_block=str_to_bool(get_config_value(args, config, 'no_logging_block', 'Logging', False)) + access_log=get_config_value( + args, config, "access_log", "Logging", "logs/access.log" + ), + block_log=get_config_value( + args, config, "block_log", "Logging", "logs/block.log" + ), + no_logging_access=str_to_bool( + get_config_value(args, config, "no_logging_access", "Logging", False) + ), + no_logging_block=str_to_bool( + get_config_value(args, config, "no_logging_block", "Logging", False) + ), ) filter_config = ProxyConfigFilter( - no_filter=str_to_bool(get_config_value(args, config, 'no_filter', 'Filtering', False)), - filter_mode=get_config_value(args, config, 'filter_mode', 'Filtering', "local"), - blocked_sites=get_config_value(args, config, 'blocked_sites', 'Filtering', "config/blocked_sites.txt"), - blocked_url=get_config_value(args, config, 'blocked_url', 'Filtering', "config/blocked_url.txt") + no_filter=str_to_bool( + get_config_value(args, config, "no_filter", "Filtering", False) + ), + filter_mode=get_config_value(args, config, "filter_mode", "Filtering", "local"), + blocked_sites=get_config_value( + args, config, "blocked_sites", "Filtering", "config/blocked_sites.txt" + ), + blocked_url=get_config_value( + args, config, "blocked_url", "Filtering", "config/blocked_url.txt" + ), ) ssl_config = ProxyConfigSSL( - ssl_inspect=str_to_bool(get_config_value(args, config, 'ssl_inspect', 'Security', False)), - inspect_ca_cert=get_config_value(args, config, 'inspect_ca_cert', 'Security', "certs/ca/cert.pem"), - inspect_ca_key=get_config_value(args, config, 'inspect_ca_key', 'Security', "certs/ca/key.pem"), - inspect_certs_folder=get_config_value(args, config, 'inspect_certs_folder', 'Security', "certs/"), - cancel_inspect=get_config_value(args, config, 'cancel_inspect', 'Security', "config/cancel_inspect.txt") + ssl_inspect=str_to_bool( + get_config_value(args, config, "ssl_inspect", "Security", False) + ), + inspect_ca_cert=get_config_value( + args, config, "inspect_ca_cert", "Security", "certs/ca/cert.pem" + ), + inspect_ca_key=get_config_value( + args, config, "inspect_ca_key", "Security", "certs/ca/key.pem" + ), + inspect_certs_folder=get_config_value( + args, config, "inspect_certs_folder", "Security", "certs/" + ), + cancel_inspect=get_config_value( + args, config, "cancel_inspect", "Security", "config/cancel_inspect.txt" + ), ) proxy = ProxyServer( @@ -66,10 +97,11 @@ def main(): authorized_ips=authorized_ips, proxy_enable=str_to_bool(proxy_enable), proxy_host=proxy_host, - proxy_port=proxy_port + proxy_port=proxy_port, ) proxy.start() + if __name__ == "__main__": main() From e2e9ed8115f76717dd61a8c99d6a5b621ffff28b Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:41:30 +0200 Subject: [PATCH 5/8] remove version.py --- .bumpversion.cfg | 2 +- .github/workflows/main.yml | 13 ++++++------- pyproxy/__init__.py | 13 +++++++++++++ pyproxy/server.py | 2 +- pyproxy/utils/args.py | 2 +- pyproxy/utils/version.py | 15 --------------- setup.py | 2 +- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8d3eec7..4122366 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,4 +3,4 @@ current_version = 0.3.2 commit = True tag = True -[bumpversion:file:pyproxy/utils/version.py] +[bumpversion:file:pyproxy/__init__.py] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 37f5c19..fa99087 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,12 +76,11 @@ jobs: - name: Log in to GitHub Container Registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Get version from version.py + - name: Get version id: get_version run: | - VERSION=$(python -c "from pyproxy.utils.version import __version__; print(__version__)") - echo "VERSION=${VERSION}" >> $GITHUB_ENV - echo "Docker tag version: $VERSION" + version=$(grep '^__version__' pyproxy/__init__.py | cut -d'"' -f2) + echo "version=${version}" >> $GITHUB_ENV - name: Convert repository owner to lowercase run: echo "REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV @@ -115,11 +114,11 @@ jobs: with: python-version: 3.13 - - name: Get version from version.py + - name: Get version id: get_version run: | - VERSION=$(python -c "from pyproxy.utils.version import __version__; print(__version__)") - echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + version=$(grep '^__version__' pyproxy/__init__.py | cut -d'"' -f2) + echo "version=${version}" >> $GITHUB_OUTPUT - name: Create Tag run: | diff --git a/pyproxy/__init__.py b/pyproxy/__init__.py index e69de29..923f5e2 100644 --- a/pyproxy/__init__.py +++ b/pyproxy/__init__.py @@ -0,0 +1,13 @@ +""" +This module defines the version of the application. It contains a single constant +that holds the current version number of the application. +""" + +import os + +__version__ = "0.3.2" + +if os.path.isdir("pyproxy/monitoring"): + __slim__ = False +else: + __slim__ = True diff --git a/pyproxy/server.py b/pyproxy/server.py index d4a39ef..e268efe 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -15,7 +15,7 @@ import time import ipaddress -from pyproxy.utils.version import __slim__ +from pyproxy import __slim__ from pyproxy.utils.logger import configure_file_logger, configure_console_logger from pyproxy.handlers.client import ProxyHandlers from pyproxy.modules.filter import filter_process diff --git a/pyproxy/utils/args.py b/pyproxy/utils/args.py index 0a60790..c20bab5 100644 --- a/pyproxy/utils/args.py +++ b/pyproxy/utils/args.py @@ -8,7 +8,7 @@ import argparse import os from rich_argparse import MetavarTypeRichHelpFormatter -from pyproxy.utils.version import __version__ +from pyproxy import __version__ def parse_args() -> argparse.Namespace: diff --git a/pyproxy/utils/version.py b/pyproxy/utils/version.py index 27df6cb..e69de29 100644 --- a/pyproxy/utils/version.py +++ b/pyproxy/utils/version.py @@ -1,15 +0,0 @@ -""" -pyproxy.utils.version.py - -This module defines the version of the application. It contains a single constant -that holds the current version number of the application. -""" - -import os - -__version__ = "0.3.2" - -if os.path.isdir("pyproxy/monitoring"): - __slim__ = False -else: - __slim__ = True diff --git a/setup.py b/setup.py index 5d87913..8b42c45 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ def get_version(): base_dir = os.path.dirname(__file__) - version_path = os.path.join(base_dir, "pyproxy", "utils", "version.py") + version_path = os.path.join(base_dir, "pyproxy", "__init__.py") version_ns = {} with open(version_path, "r") as f: exec(f.read(), version_ns) From ac12b3a0dd9bb8927de7ac0ed9ce5fd54ad19d5c Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:49:19 +0200 Subject: [PATCH 6/8] rename package to pyproxytools --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a8ae593..d48b162 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "pyproxyx" +name = "pyproxytools" description = "Lightweight and fast python web proxy" readme = "README.md" license = "MIT" From 5e4c7b7aec6ed6c1f07bc9c7cd37725165cde39e Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:50:11 +0200 Subject: [PATCH 7/8] readme: add install from package --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 85514a4..6200522 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ ## 📦 **Installation** +### Install from package +```bash +pip install pyproxytools +``` + ### Install from source ```bash git clone https://github.com/6C656C65/pyproxy.git From 021ea7af8f790f8ecf35119aaef937252fc8ce1c Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sat, 31 May 2025 00:50:17 +0200 Subject: [PATCH 8/8] =?UTF-8?q?Bump=20version:=200.3.2=20=E2=86=92=200.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pyproxy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4122366..031db97 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.2 +current_version = 0.4.0 commit = True tag = True diff --git a/pyproxy/__init__.py b/pyproxy/__init__.py index 923f5e2..b5de7b7 100644 --- a/pyproxy/__init__.py +++ b/pyproxy/__init__.py @@ -5,7 +5,7 @@ import os -__version__ = "0.3.2" +__version__ = "0.4.0" if os.path.isdir("pyproxy/monitoring"): __slim__ = False