diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index b814208f01..2fa2380906 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ --- -open_collective: proxypy -... +# These are supported funding model platforms + +github: abhinavsingh diff --git a/.github/workflows/test-library.yml b/.github/workflows/test-library.yml index 2f4a94e194..4f49831221 100644 --- a/.github/workflows/test-library.yml +++ b/.github/workflows/test-library.yml @@ -72,7 +72,7 @@ jobs: container-platforms: ${{ steps.container.outputs.platforms }} steps: - name: Switch to using Python 3.9 by default - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.9 - name: >- @@ -135,7 +135,7 @@ jobs: - name: Set up pip cache if: >- steps.request-check.outputs.release-requested != 'true' - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.4 with: path: ${{ steps.pip-cache-dir.outputs.dir }} key: >- @@ -225,7 +225,7 @@ jobs: steps: - name: Switch to using Python v3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.10' - name: >- @@ -244,7 +244,7 @@ jobs: run: >- echo "::set-output name=dir::$(pip cache dir)" - name: Set up pip cache - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.4 with: path: ${{ steps.pip-cache.outputs.dir }} key: >- @@ -284,7 +284,7 @@ jobs: if: >- fromJSON(needs.pre-setup.outputs.is-untagged-devel) || fromJSON(needs.pre-setup.outputs.release-requested) - uses: fregante/setup-git-user@v1.0.1 + uses: fregante/setup-git-user@v1.1.0 - name: >- Tag the release in the local Git repo as ${{ needs.pre-setup.outputs.git-tag }} @@ -350,7 +350,7 @@ jobs: steps: - name: Switch to using Python v3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.10' - name: >- @@ -369,7 +369,7 @@ jobs: run: >- echo "::set-output name=dir::$(pip cache dir)" - name: Set up pip cache - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.4 with: path: ${{ steps.pip-cache.outputs.dir }} key: >- @@ -467,7 +467,7 @@ jobs: steps: - name: Switch to using Python v${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: >- @@ -486,7 +486,7 @@ jobs: run: >- echo "::set-output name=dir::$(pip cache dir)" - name: Set up pip cache - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.4 with: path: ${{ steps.pip-cache.outputs.dir }} key: >- @@ -670,7 +670,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Brew @@ -724,7 +724,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install Pip Dependencies @@ -1057,7 +1057,7 @@ jobs: fetch-depth: 1 ref: ${{ github.event.inputs.release-commitish }} - name: Setup git user as [bot] - uses: fregante/setup-git-user@v1.0.1 + uses: fregante/setup-git-user@v1.1.0 - name: >- Tag the release in the local Git repo diff --git a/README.md b/README.md index 273a6bcece..a0a2f9b49b 100644 --- a/README.md +++ b/README.md @@ -2315,9 +2315,9 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT] [--filtered-client-ips FILTERED_CLIENT_IPS] [--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG] -proxy.py v2.4.2.dev11+g0beb02d.d20220420 +proxy.py v2.4.3.dev14+gc6b2de6.d20220605 -options: +optional arguments: -h, --help show this help message and exit --tunnel-hostname TUNNEL_HOSTNAME Default: None. Remote hostname or IP address to which @@ -2334,11 +2334,11 @@ options: --tunnel-remote-port TUNNEL_REMOTE_PORT Default: 8899. Remote port which will be forwarded locally for proxy. - --threadless Default: True. Enabled by default on Python 3.8+ (mac, - linux). When disabled a new thread is spawned to + --threadless Default: False. Enabled by default on Python 3.8+ + (mac, linux). When disabled a new thread is spawned to handle each client connection. - --threaded Default: False. Disabled by default on Python < 3.8 - and windows. When enabled a new thread is spawned to + --threaded Default: True. Disabled by default on Python < 3.8 and + windows. When enabled a new thread is spawned to handle each client connection. --num-workers NUM_WORKERS Defaults to number of CPU cores. @@ -2434,8 +2434,8 @@ options: Default: None. Signing certificate to use for signing dynamically generated HTTPS certificates. If used, must also pass --ca-key-file and --ca-signing-key-file - --ca-file CA_FILE Default: /Users/abhinavsingh/Dev/proxy.py/venv310/lib/ - python3.10/site-packages/certifi/cacert.pem. Provide + --ca-file CA_FILE Default: /Users/abhinavsingh/Dev/proxy.py/venv373/lib/ + python3.7/site-packages/certifi/cacert.pem. Provide path to custom CA bundle for peer certificate verification --ca-signing-key-file CA_SIGNING_KEY_FILE diff --git a/benchmark/requirements.txt b/benchmark/requirements.txt index 3da05de1a1..fa0ad1432f 100644 --- a/benchmark/requirements.txt +++ b/benchmark/requirements.txt @@ -1,5 +1,5 @@ aiohttp==3.8.1 -blacksheep==1.2.2 -starlette==0.17.1 +blacksheep==1.2.7 +starlette==0.19.1 tornado==6.1 uvicorn==0.16.0 diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index b9445ccfec..06c866ac7f 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -32,9 +32,9 @@ "rollup-plugin-copy": "^3.4.0", "rollup-plugin-javascript-obfuscator": "^1.0.4", "rollup-plugin-typescript": "^1.0.1", - "ts-node": "^10.7.0", + "ts-node": "^10.8.0", "typescript": "^4.5.4", - "ws": "^8.5.0" + "ws": "^8.6.0" } }, "node_modules/@babel/code-frame": { @@ -63,27 +63,43 @@ "js-tokens": "^4.0.0" } }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { "node": ">=12" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -4789,12 +4805,12 @@ } }, "node_modules/ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", + "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", "dev": true, "dependencies": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -4805,7 +4821,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "bin": { @@ -5223,9 +5239,9 @@ } }, "node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -5363,19 +5379,35 @@ "js-tokens": "^4.0.0" } }, - "@cspotcode/source-map-consumer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", "dev": true }, - "@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "requires": { - "@cspotcode/source-map-consumer": "0.8.0" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, "@nodelib/fs.scandir": { @@ -8991,12 +9023,12 @@ } }, "ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", + "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", "dev": true, "requires": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -9007,7 +9039,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "dependencies": { @@ -9324,9 +9356,9 @@ } }, "ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", "dev": true, "requires": {} }, diff --git a/dashboard/package.json b/dashboard/package.json index eac02ae6ab..d3b3c440a3 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -48,8 +48,8 @@ "rollup-plugin-copy": "^3.4.0", "rollup-plugin-javascript-obfuscator": "^1.0.4", "rollup-plugin-typescript": "^1.0.1", - "ts-node": "^10.7.0", + "ts-node": "^10.8.0", "typescript": "^4.5.4", - "ws": "^8.5.0" + "ws": "^8.6.0" } } diff --git a/docs/conf.py b/docs/conf.py index 20d20df6c9..40573d5edc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -320,6 +320,7 @@ (_py_class_role, 'T'), (_py_class_role, 'HostPort'), (_py_class_role, 'TcpOrTlsSocket'), + (_py_class_role, 're.Pattern'), (_py_obj_role, 'proxy.core.work.threadless.T'), (_py_obj_role, 'proxy.core.work.work.T'), (_py_obj_role, 'proxy.core.base.tcp_server.T'), diff --git a/proxy/common/types.py b/proxy/common/types.py index 603dd9524d..984cc3bdd3 100644 --- a/proxy/common/types.py +++ b/proxy/common/types.py @@ -8,7 +8,9 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import re import ssl +import sys import queue import socket import ipaddress @@ -30,3 +32,10 @@ IpAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] TcpOrTlsSocket = Union[ssl.SSLSocket, socket.socket] HostPort = Tuple[str, int] + +if sys.version_info.minor == 6: + RePattern = Any +elif sys.version_info.minor in (7, 8): + RePattern = re.Pattern # type: ignore +else: + RePattern = re.Pattern[Any] # type: ignore diff --git a/proxy/http/server/plugin.py b/proxy/http/server/plugin.py index 0115558c18..4e290178f4 100644 --- a/proxy/http/server/plugin.py +++ b/proxy/http/server/plugin.py @@ -11,14 +11,16 @@ import argparse import mimetypes from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union, Optional +from proxy.http.url import Url from ..parser import HttpParser from ..responses import NOT_FOUND_RESPONSE_PKT, okResponse from ..websocket import WebsocketFrame from ..connection import HttpClientConnection from ...core.event import EventQueue from ..descriptors import DescriptorsHandlerMixin +from ...common.types import RePattern from ...common.utils import bytes_ @@ -127,6 +129,36 @@ class ReverseProxyBasePlugin(ABC): """ReverseProxy base plugin class.""" @abstractmethod - def routes(self) -> List[Tuple[str, List[bytes]]]: - """Return List(path, List(upstream)) reverse proxy config.""" + def routes(self) -> List[Union[str, Tuple[str, List[bytes]]]]: + """List of routes registered by plugin. + + There are 2 types of routes: + + 1) Dynamic routes (str): Should be a regular expression + 2) Static routes (tuple): Contain 2 elements, a route regular expression + and list of upstream urls to serve when the route matches. + + Static routes doesn't require you to implement the `handle_route` method. + Reverse proxy core will automatically pick one of the configured upstream URL + and serve it out-of-box. + + Dynamic routes are helpful when you want to dynamically match and serve upstream urls. + To handle dynamic routes, you must implement the `handle_route` method, which + must return the url to serve.""" raise NotImplementedError() # pragma: no cover + + def handle_route(self, request: HttpParser, pattern: RePattern) -> Url: + """Implement this method if you have configured dynamic routes.""" + pass + + def regexes(self) -> List[str]: + """Helper method to return list of route regular expressions.""" + routes = [] + for route in self.routes(): + if isinstance(route, str): + routes.append(route) + elif isinstance(route, tuple): + routes.append(route[0]) + else: + raise ValueError('Invalid route type') + return routes diff --git a/proxy/http/server/reverse.py b/proxy/http/server/reverse.py index 45afe1f91f..63bd3d6537 100644 --- a/proxy/http/server/reverse.py +++ b/proxy/http/server/reverse.py @@ -38,37 +38,39 @@ class ReverseProxy(TcpUpstreamConnectionHandler, HttpWebServerBasePlugin): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.choice: Optional[Url] = None - self.reverse: Dict[str, List[bytes]] = {} + self.plugins: List['ReverseProxyBasePlugin'] = [] + for klass in self.flags.plugins[b'ReverseProxyBasePlugin']: + plugin: 'ReverseProxyBasePlugin' = klass() + self.plugins.append(plugin) def handle_upstream_data(self, raw: memoryview) -> None: self.client.queue(raw) def routes(self) -> List[Tuple[int, str]]: - reverse: List[Tuple[str, List[bytes]]] = [] - for klass in self.flags.plugins[b'ReverseProxyBasePlugin']: - instance: 'ReverseProxyBasePlugin' = klass() - reverse.extend(instance.routes()) r = [] - for (route, upstreams) in reverse: - r.append((httpProtocolTypes.HTTP, route)) - r.append((httpProtocolTypes.HTTPS, route)) - self.reverse[route] = upstreams + for plugin in self.plugins: + for route in plugin.regexes(): + r.append((httpProtocolTypes.HTTP, route)) + r.append((httpProtocolTypes.HTTPS, route)) return r def handle_request(self, request: HttpParser) -> None: - # TODO: Core must be capable of dispatching a context - # with each invocation of handle request callback. - # - # Example, here we don't know which of our registered - # route actually matched. - # - for route in self.reverse: - pattern = re.compile(route) - if pattern.match(text_(request.path)): - self.choice = Url.from_bytes( - random.choice(self.reverse[route]), - ) - break + for plugin in self.plugins: + for route in plugin.routes(): + if isinstance(route, tuple): + pattern = re.compile(route[0]) + if pattern.match(text_(request.path)): + self.choice = Url.from_bytes( + random.choice(route[1]), + ) + break + elif isinstance(route, str): + pattern = re.compile(route) + if pattern.match(text_(request.path)): + self.choice = plugin.handle_route(request, pattern) + break + else: + raise ValueError('Invalid route') assert self.choice and self.choice.hostname port = self.choice.port or \ DEFAULT_HTTP_PORT \ @@ -85,12 +87,7 @@ def handle_request(self, request: HttpParser) -> None: ), as_non_blocking=True, ) - # Update Host header - # if request.has_header(b'Host'): - # request.del_header(b'Host') - # request.add_header( - # b'Host', ('%s:%d' % self.upstream.addr).encode('utf-8'), - # ) + request.path = self.choice.remainder self.upstream.queue(memoryview(request.build())) except ConnectionRefusedError: raise HttpProtocolException( # pragma: no cover diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index 0d89ba86e7..c3ad91945b 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -15,6 +15,7 @@ onmessage httpbin localhost + Lua """ from .cache import CacheResponsesPlugin, BaseCacheResponsesPlugin from .shortlink import ShortLinkPlugin diff --git a/proxy/plugin/reverse_proxy.py b/proxy/plugin/reverse_proxy.py index 01eca2b09d..5d2530eda1 100644 --- a/proxy/plugin/reverse_proxy.py +++ b/proxy/plugin/reverse_proxy.py @@ -7,23 +7,19 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. -""" -from typing import List, Tuple -from ..http.server import ReverseProxyBasePlugin + .. spelling:: + Lua +""" +import re +from typing import List, Tuple, Union -# TODO: We must use nginx python parser and -# make this plugin nginx.conf complaint. -REVERSE_PROXY_LOCATION: str = r'/get$' -# Randomly choose either http or https upstream endpoint. -# -# This is just to demonstrate that both http and https upstream -# reverse proxy works. -REVERSE_PROXY_PASS = [ - b'http://httpbin.org/get', - b'https://httpbin.org/get', -] +from ..http import Url +from ..http.parser import HttpParser +from ..http.server import ReverseProxyBasePlugin +from ..common.types import RePattern +from ..http.exception.base import HttpProtocolException class ReverseProxyPlugin(ReverseProxyBasePlugin): @@ -35,8 +31,38 @@ class ReverseProxyPlugin(ReverseProxyBasePlugin): } ``` - Update the routes config before. + Plugin also demonstrates how to write "Python" equivalent for any + "Nginx Lua" based configuration i.e. your plugin code will have + full control over what do after one of your route has matched. """ - def routes(self) -> List[Tuple[str, List[bytes]]]: - return [(REVERSE_PROXY_LOCATION, REVERSE_PROXY_PASS)] + def routes(self) -> List[Union[str, Tuple[str, List[bytes]]]]: + return [ + # A static route + ( + r'/get$', + [b'http://httpbin.org/get', b'https://httpbin.org/get'], + ), + # A dynamic route to catch requests on "/get/"" + # See "handle_route" method below for what we do when + # this pattern matches. + r'/get/(\d+)$', + ] + + def handle_route(self, request: HttpParser, pattern: RePattern) -> Url: + """For our example dynamic route, we want to simply convert + any incoming request to "/get/1" into "/get?id=1" when serving from upstream. + """ + choice: Url = Url.from_bytes(b'http://httpbin.org/get') + assert request.path + result = re.search(pattern, request.path.decode()) + if not result or len(result.groups()) != 1: + raise HttpProtocolException('Invalid request') + assert choice.remainder == b'/get' + # NOTE: Internally, reverse proxy core replaces + # original request.path with the choice.remainder value. + # e.g. for this example, request.path will be "/get/1". + # Core will automatically replace that with "/get?id=1" + # before dispatching request to choice of upstream server. + choice.remainder += f'?id={result.groups()[0]}'.encode() + return choice diff --git a/requirements-testing.txt b/requirements-testing.txt index 1ffbbcfd0b..200567b70c 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -2,19 +2,19 @@ wheel==0.37.1 python-coveralls==2.9.3 coverage==6.2 flake8==4.0.1 -pytest==6.2.5 +pytest==7.0.1 pytest-cov==3.0.0 pytest-xdist == 2.5.0 pytest-mock==3.6.1 pytest-asyncio==0.16.0 autopep8==1.6.0 -mypy==0.940 -py-spy==0.3.11 +mypy==0.961 +py-spy==0.3.12 codecov==2.1.12 tox==3.25.0 mccabe==0.6.1 pylint==2.13.7 -rope==0.22.0 +rope==1.1.1 # Required by test_http2.py httpx==0.22.0 h2==4.1.0