diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75e607fd..5f3ea492 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,16 +1,11 @@ -name: Build Pacparser +name: Build Pacparser for Mac Universal on: push: - branches: [main] - paths: - - "src/**" - - ".github/**" - - Makefile - - Dockerfile + branches: + - preveil + - feature/* pull_request: - release: - types: [published] workflow_dispatch: inputs: tag: @@ -23,280 +18,39 @@ permissions: jobs: build: + runs-on: macos-13 strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-13, macos-latest] - include: - - os: ubuntu-latest - os_suffix: "ubuntu-x86_64" - - os: macos-latest - os_suffix: "macos-arm64" - - os: macos-13 - os_suffix: "macos-x86_64" - - os: windows-latest - os_suffix: "windows-x86_64" - runs-on: ${{ matrix.os }} + python-version: ["3.12", "3.13"] steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@v5 if: ${{ !contains(github.event_name, 'workflow_dispatch') }} with: fetch-depth: 0 - name: Check out code for workflow_dispatch if: ${{ contains(github.event_name, 'workflow_dispatch') }} - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - fetch-depth: 0 - ref: ${{ github.event.inputs.tag }} - - - name: make non-windows - if: ${{ matrix.os != 'windows-latest' }} - run: make -C src - - - name: make windows - if: ${{ matrix.os == 'windows-latest' }} - run: make -C src -f Makefile.win32 - - - name: Get ref_name - id: get_ref_name - if: ${{ matrix.os != 'windows-latest' }} - run: | - if [ "${{ github.event_name }}" == "pull_request" ]; then - echo "ref_name=${{ github.base_ref }}" - echo "ref_name=${{ github.base_ref }}" >> $GITHUB_OUTPUT || exit 1 - else - echo "ref_name=${{ github.ref_name }}" - echo "ref_name=${{ github.ref_name }}" >> $GITHUB_OUTPUT || exit 1 - fi - - - name: make non-windows dist - if: ${{ matrix.os != 'windows-latest' }} - run: | - DIST_OS_SUFFIX=${{ matrix.os_suffix }} make -C src dist - ls -R src/*.zip - - - name: make windows dist - if: ${{ matrix.os == 'windows-latest' }} - run: | - make -C src -f Makefile.win32 dist - - - name: Upload dist (non-windows) - if: ${{ matrix.os != 'windows-latest' }} - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 - with: - name: pacparser-dist-${{ matrix.os }} - path: src/pacparser*.zip - - - name: Upload dist (windows) - if: ${{ matrix.os == 'windows-latest' }} - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 - with: - name: pacparser-dist-${{ matrix.os }} - path: src/dist - - build-release-binaries: - needs: build - runs-on: ubuntu-latest - steps: - - name: Download ubuntu cloudprober binaries - uses: actions/download-artifact@v4 - with: - pattern: pacparser-dist-* - - - name: Fix files - run: | - ls -R . - mv pacparser-dist-*/*.zip . - file=$(ls pacparser-*-ubuntu*.zip) - name=${file/ubuntu/windows} - name=${name/.zip/} - mv pacparser-dist-windows-latest $name - zip -r $name.zip $name - - - name: Upload pacparser release - uses: actions/upload-artifact@v4 - with: - name: pacparser-release-binaries - path: pacparser-*.zip - - python-module-build: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-13] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - architecture: [x64] - include: - - os: macos-latest - python-version: "3.11" - architecture: arm64 - - os: macos-latest - python-version: "3.12" - architecture: arm64 - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - if: ${{ !contains(github.event_name, 'workflow_dispatch') }} - with: - fetch-depth: 0 - - - name: Check out code for workflow_dispatch - if: ${{ contains(github.event_name, 'workflow_dispatch') }} - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: ${{ github.event.inputs.tag }} - name: Set up Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} - name: Set up setuptools run: | python --version - python -mpip install setuptools + python -m pip install setuptools build wheel - - name: make non-windows - if: ${{ matrix.os != 'windows-latest' }} + - name: Build a wheel run: make -C src pymod-dist - - name: make windows - if: ${{ matrix.os == 'windows-latest' }} - run: make -C src -f Makefile.win32 pymod-dist - - - name: Upload dist - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 - with: - name: - pacparser-python-${{ matrix.python-version }}-${{ matrix.os }}-dist - path: src/pymod/pacparser-python* - - - name: Build wheel non-linux - if: ${{ matrix.os != 'ubuntu-latest' }} - run: | - python -m pip install wheel - cd src/pymod && python setup.py bdist_wheel - - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: src_changes - with: - filters: | - changed: - - 'src/**' - - - name: Publish package to PyPI (non-linux) - if: | - (matrix.os != 'ubuntu-latest') && - (steps.src_changes.outputs.changed == 'true' || - startsWith(github.event.inputs.tag, 'v') || startsWith(github.ref, - 'refs/tags/v')) && (github.event_name != 'pull_request') - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI }} - run: | - python -m pip install twine - ls -R . - twine upload src/pymod/dist/* - - build-linux-wheels: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - if: ${{ !contains(github.event_name, 'workflow_dispatch') }} - with: - fetch-depth: 0 - - - name: Check out code for workflow_dispatch - if: ${{ contains(github.event_name, 'workflow_dispatch') }} - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - fetch-depth: 0 - ref: ${{ github.event.inputs.tag }} - - - name: Set env - run: | - echo "PACPARSER_VERSION=$(git describe --always --tags \ - --candidate=100)" >> $GITHUB_ENV - - - name: Set up Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 - - - name: Set up setuptools - run: | - python --version - python -mpip install setuptools - - - name: make - run: make -C src pymod - - - name: Build sdist - run: cd src/pymod && python setup.py sdist - - - name: Install cibuildwheel and twine - run: python -m pip install cibuildwheel twine - - - name: Build wheel using cibuildwheel - run: | - cp src/spidermonkey/libjs.a src/pacparser.o src/pacparser.h src/pymod - cd src/pymod && python -m cibuildwheel --output-dir dist - env: - CIBW_BUILD: "cp{37,38,39,310,311,312}-manylinux*64" - CIBW_ENVIRONMENT: "PACPARSER_VERSION=${{ env.PACPARSER_VERSION }}" - - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: src_changes - with: - filters: | - changed: - - 'src/**' - - - name: Publish package to PyPI - if: | - startsWith(github.event.inputs.tag, 'v') || - startsWith(github.ref,'refs/tags/v') ||steps.src_changes.outputs.changed == 'true' - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI }} - run: | - twine upload src/pymod/dist/* - - build_and_push_docker_multiarch: - name: Build and push multiarch docker image - if: | - github.repository == 'manugarg/pacparser' && - (github.ref == 'refs/heads/main' || startswith(github.ref, 'refs/heads/docker') || - startsWith(github.ref, 'refs/tags/v')) - runs-on: ubuntu-latest - steps: - - name: Check out code into the Go module directory - if: ${{ !contains(github.event_name, 'workflow_dispatch') }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check out code into the Go module directory - if: ${{ contains(github.event_name, 'workflow_dispatch') }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.inputs.tag }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GHCR - uses: docker/login-action@v3 + - name: Upload the wheel + uses: actions/upload-artifact@v4 with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push release Docker Image (main-ghcr) - run: make docker_multiarch DOCKER_IMAGE=ghcr.io/manugarg/pactester + name: pacparser-wheel-py${{ matrix.python-version }} + path: src/pymod/dist/pacparser*.whl diff --git a/README.md b/README.md index 39a5083a..08a9d010 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,10 @@ pacparser itself. As a result, pacparser is as close to standard as it gets :) ### Install -For Python module, you can use pip. Pre-built module is available for `64-bit Linux, -Windows, MacOS-Intel, and MacOS-ARM`, for Python `3.7, 3.8, 3.9, 3.10 and 3.11`. +For Python module, you can use pip. Pre-built module is available for `64-bit MacOS Universal (Intel & ARM)`, for Python `3.12 and 3.13`. ``` python -m pip install pacparser -python -m pip install pacparser==1.3.8.dev15 (specific version) +python -m pip install pacparser==2.0.1 (specific version) ``` For other pre-built binaries, download them from the project's [releases]( diff --git a/src/Makefile b/src/Makefile index 3f7e6a5b..352ffe8d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -20,19 +20,21 @@ # This file is not part of the source code repository. It's generated by the # packaging script. --include version.mk +include version.mk ifeq ($(shell uname),FreeBSD) SHELL := /usr/local/bin/bash else SHELL := /bin/bash endif +$(info SHELL = $(SHELL)) VERSION ?= $(shell git describe --always --tags --candidate=100) - -GIT_TAG := $(shell git describe --exact-match --exclude tip --tags HEAD 2>/dev/null || /bin/true) - +$(info VERSION = $(VERSION)) +GIT_TAG := $(shell git describe --exact-match --exclude tip --tags HEAD 2>/dev/null || true) +$(info GIT_TAG = $(GIT_TAG)) OS_ARCH := $(subst /,_,$(shell uname -s | sed /\ /s//_/)) +$(info OS_ARCH = $(OS_ARCH)) LIBRARY_NAME = libpacparser LIB_VER = 1 @@ -57,9 +59,9 @@ endif ifeq ($(OS_ARCH),Darwin) PREFIX ?= /usr/local - MAC_MAJOR_VERSION := $(shell sw_vers -productVersion | cut -d. -f1) - MAC_MAJOR_VERSION := 11.0 - MAC_GT_OS11 := $(shell [ $(MAC_MAJOR_VERSION) -le 10 ] && echo false) + #MAC_MAJOR_VERSION := $(shell sw_vers -productVersion | cut -d. -f1) + MAC_MAJOR_VERSION := 11 + MAC_GT_OS11 := $(shell [ $(MAC_MAJOR_VERSION) -gt 11 ] && echo true || echo false) SO_SUFFIX = dylib LIBRARY = $(LIBRARY_NAME).$(LIB_VER).$(SO_SUFFIX) MKSHLIB = $(CC) -dynamiclib -framework System @@ -80,14 +82,21 @@ PREFIX ?= /usr MAINT_CFLAGS := -g -DXP_UNIX -Wall -DVERSION=$(VERSION) ifndef PYTHON - PYTHON = python + PYTHON = python3 endif +$(info PYTHON = $(PYTHON)) +$(info LIBRARY = $(LIBRARY)) +$(info LIB_OPTS = $(LIB_OPTS)) +$(info -------------------------) + # Spidermonkey library. MAINT_CFLAGS += -Ispidermonkey/js/src LIBRARY_LINK = $(LIBRARY_NAME).$(SO_SUFFIX) PREFIX := $(DESTDIR)$(PREFIX) +$(info PREFIX = $(PREFIX)) + LIB_PREFIX = $(PREFIX)/lib INC_PREFIX = $(PREFIX)/include BIN_PREFIX = $(PREFIX)/bin diff --git a/src/Makefile.win32 b/src/Makefile.win32 index 8dedb102..e6b00de7 100644 --- a/src/Makefile.win32 +++ b/src/Makefile.win32 @@ -22,7 +22,7 @@ # packaging script. -include version.mk -ifeq ($(OS),Windows_NT) +ifeq ($(OS),Windows_NT) RM = del /Q /F CP = copy /Y ifdef ComSpec @@ -41,7 +41,7 @@ VERSION ?= $(shell git describe --always --tags --candidate=100) LIB_VER=1 CFLAGS=-g -DXP_WIN -DWINVER=0x0501 -DVERSION=$(VERSION) -Ispidermonkey/js/src -Wall CC=gcc -PYTHON ?= python +PYTHON ?= python3 .PHONY: clean pymod install-pymod diff --git a/src/pymod/pacparser/__init__.py b/src/pymod/pacparser/__init__.py index b4a81e1f..0a490542 100644 --- a/src/pymod/pacparser/__init__.py +++ b/src/pymod/pacparser/__init__.py @@ -33,7 +33,7 @@ import re import sys -_URL_REGEX = re.compile('^[^:]*:\/\/([^\/:]+)') +_URL_REGEX = re.compile(r'^[^:]*://([^/:]+)') class URLError(Exception): def __init__(self, url): diff --git a/src/pymod/setup.py b/src/pymod/setup.py index b3baaea6..ebe12468 100644 --- a/src/pymod/setup.py +++ b/src/pymod/setup.py @@ -64,25 +64,28 @@ def sanitize_version(ver): def git_version(): return sanitize_version( subprocess.check_output( - "git describe --always --tags --candidate=100".split(" "), text=True + "git describe --always --tags".split(" "), text=True ) ) def pacparser_version(): - if ( - subprocess.call("git rev-parse --git-dir".split(" "), stderr=subprocess.DEVNULL) - == 0 - ): - return git_version() - - # Check if we have version.mk. It's added in the manual release tarball. version_file = os.path.join(setup_dir(), "..", "version.mk") - if os.path.exists(version_file): - with open(version_file) as f: - return sanitize_version(f.read().replace("VERSION=", "")) - - return sanitize_version(os.environ.get("PACPARSER_VERSION", "1.0.0")) + with open(version_file) as f: + v = f.read().replace("VERSION=", "").strip() + return v + # if ( + # subprocess.call("git rev-parse --git-dir".split(" "), stderr=subprocess.DEVNULL) + # == 0 + # ): + # return git_version() + # # Check if we have version.mk. It's added in the manual release tarball. + # version_file = os.path.join(setup_dir(), "..", "version.mk") + # print(f"version_file={version_file}") + # if os.path.exists(version_file): + # with open(version_file) as f: + # return sanitize_version(f.read().replace("VERSION=", "")) + # return sanitize_version(os.environ.get("PACPARSER_VERSION", "1.0.0")) class DistCmd(setuptools.Command): @@ -163,6 +166,7 @@ def main(patched_func): }, name="pacparser", version=pacparser_version(), + python_requires=">=3.12", description="Pacparser package", author="Manu Garg", author_email="manugarg@gmail.com", diff --git a/src/version.mk b/src/version.mk new file mode 100644 index 00000000..f9a2e772 --- /dev/null +++ b/src/version.mk @@ -0,0 +1 @@ +VERSION=2.0.1 diff --git a/tools/cleanmac.sh b/tools/cleanmac.sh new file mode 100755 index 00000000..507fc9de --- /dev/null +++ b/tools/cleanmac.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +rm -rf \ + rust_ffi/target/ \ + src/jsapi_buildstamp \ + src/libpacparser.1.dylib \ + src/libpacparser.dylib \ + src/pacparser.o \ + src/pymod/pacparser_o_buildstamp \ + src/spidermonkey/js-buildstamp \ + src/spidermonkey/js/src/Darwin_DBG.OBJ/ \ + src/spidermonkey/js/src/jsautocfg.h \ + src/spidermonkey/libjs.a \ + src/pymod/build/ \ + src/pymod/dist/ \ + src/pymod/pacparser.egg-info/ diff --git a/tools/sample-pac.py b/tools/sample-pac.py new file mode 100644 index 00000000..6ef26404 --- /dev/null +++ b/tools/sample-pac.py @@ -0,0 +1,68 @@ +""" +This sample PAC string demonstrates various common PAC file features: + +Direct connections for local/internal IP ranges (10.x.x.x, 172.16.x.x, 192.168.x.x, 127.x.x.x) +Specific proxy routing for internal corporate domains +Blocking domains by routing to a black hole proxy +Protocol-based routing (different handling for HTTP vs HTTPS) +SOCKS proxy usage for specific sites +Failover proxies (multiple proxies with fallback to DIRECT) +Time-based load balancing between proxies + +The PAC file uses standard PAC functions like: + +isPlainHostName() - checks if hostname has no dots +shExpMatch() - shell expression matching (wildcards) +isInNet() - checks if IP is in a network range +dnsResolve() - resolves hostname to IP +dnsDomainIs() - checks if host is in a domain +dateRange() - checks date ranges + +This should provide a good test case for your Python PAC parser with various routing rules and PAC functions to parse. +""" + +pac = """function FindProxyForURL(url, host) { + // If the hostname matches local addresses, go direct + if (isPlainHostName(host) || + shExpMatch(host, "*.local") || + isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") || + isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") || + isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0") || + isInNet(dnsResolve(host), "127.0.0.0", "255.255.255.0")) { + return "DIRECT"; + } + + // Use specific proxy for internal corporate domains + if (dnsDomainIs(host, ".internal.company.com") || + dnsDomainIs(host, ".corp.example.com")) { + return "PROXY internal-proxy.company.com:8080"; + } + + // Block certain domains + if (shExpMatch(host, "*.blocked-site.com") || + shExpMatch(host, "*.ads.example.com")) { + return "PROXY 127.0.0.1:9999"; // Black hole proxy + } + + // Use different proxies based on protocol + if (url.substring(0, 5) == "http:") { + return "PROXY http-proxy.company.com:3128; DIRECT"; + } + + if (url.substring(0, 6) == "https:") { + return "PROXY https-proxy.company.com:3129; PROXY backup-proxy.company.com:3128; DIRECT"; + } + + // Special handling for specific sites + if (dnsDomainIs(host, ".youtube.com") || + dnsDomainIs(host, ".googlevideo.com")) { + return "SOCKS5 socks-proxy.company.com:1080"; + } + + // Load balance between multiple proxies for general traffic + if (dateRange("MON", "WED")) { + return "PROXY proxy1.company.com:8080; PROXY proxy2.company.com:8080; DIRECT"; + } else { + return "PROXY proxy2.company.com:8080; PROXY proxy1.company.com:8080; DIRECT"; + } +}"""