vpndetection-flask 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ['3.11', '3.12', '3.13']
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - uses: actions/setup-python@v7
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ cache: pip
20
+ cache-dependency-path: pyproject.toml
21
+ - run: pip install -e ".[dev]"
22
+ - run: ruff check .
23
+ - run: ruff format --check .
24
+ - run: mypy src
25
+ - run: pytest
@@ -0,0 +1,36 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ # PyPI trusted publishing exchanges this OIDC token for a short-lived upload
13
+ # token. No API token in a repository secret.
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v7
18
+ with:
19
+ python-version: '3.13'
20
+ cache: pip
21
+ cache-dependency-path: pyproject.toml
22
+ # The tag only TRIGGERS this; PyPI publishes whatever the built artifact says.
23
+ # Without this check, tagging v1.0.1 against a 1.0.0 package silently
24
+ # republishes 1.0.0, or fails as "already exists" with a confusing message.
25
+ - name: Tag must match the package version
26
+ run: |
27
+ tag="${GITHUB_REF_NAME#v}"
28
+ pkg="$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/vpndetection_flask/__init__.py)"
29
+ if [ "$tag" != "$pkg" ] ; then
30
+ echo "tag $tag does not match __version__ $pkg" >&2
31
+ exit 1
32
+ fi
33
+ - run: pip install -e ".[dev]" build
34
+ - run: pytest
35
+ - run: python -m build
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+
4
+ # Build output.
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+
9
+ # Virtualenvs, including any made to run codegen. Keep them out of the tree entirely.
10
+ .venv/
11
+ venv/
12
+ .venv-*/
13
+
14
+ # Tool caches.
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mslm Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.5
2
+ Name: vpndetection-flask
3
+ Version: 1.0.0
4
+ Summary: Official Flask extension for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN visitors on every request.
5
+ Project-URL: Homepage, https://vpndetection.io
6
+ Project-URL: Source, https://github.com/vpndetection-io/sdk-python-flask
7
+ Author-email: Mslm Dev <support@vpndetection.io>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: fraud-prevention,ip-intelligence,middleware,proxy-detection,tor,vpn,vpn-detection
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Internet
19
+ Classifier: Topic :: Security
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: flask>=3.0
23
+ Requires-Dist: vpndetection<4,>=2.1.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.11; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: ruff>=0.6; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Flask Extension
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/vpndetection-flask.svg)](https://pypi.org/project/vpndetection-flask/)
33
+ [![license](https://img.shields.io/pypi/l/vpndetection-flask.svg)](LICENSE)
34
+
35
+ The official Flask Extension for the [VPNDetection](https://vpndetection.io) API.
36
+
37
+ It classifies the visitor behind each request — VPN, residential proxy, Tor, hosting, CDN, relay — and hands the answer to your code. Blocking is opt-in.
38
+
39
+ ## Getting Started
40
+
41
+ ```bash
42
+ pip install vpndetection-flask
43
+ ```
44
+
45
+ Requires Python 3.11 or newer.
46
+
47
+ You need an API key. Create one in the [console](https://app.vpndetection.io); the free tier's allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
48
+
49
+ ```python
50
+ from flask import Flask
51
+ from vpndetection_flask import VPNDetection
52
+
53
+ app = Flask(__name__)
54
+ VPNDetection(app, api_key=os.environ["VPNDETECTION_API_KEY"])
55
+ ```
56
+
57
+ `init_app(app)` is supported for the application-factory pattern.
58
+
59
+ ```python
60
+ from vpndetection_flask import lookup
61
+
62
+
63
+ @app.route("/")
64
+ def index():
65
+ found = lookup()
66
+ return "Hello, VPN user" if found.result.is_vpn else "Hello"
67
+ ```
68
+
69
+ By default nothing is blocked. Every request gets an answer and your own code decides what that means — which is usually what you want, because whether a VPN visitor is a problem depends entirely on what they are doing.
70
+
71
+ ## Blocking
72
+
73
+ Pass a `block_condition` and a matching request is answered with `403` and never reaches your code.
74
+
75
+ ```python
76
+ block_condition = {"is_vpn": True}
77
+ ```
78
+
79
+ A condition is written in the shape of a result, keyed by the same names the API uses, and only the members you name are considered. That lets it reach the evidence, not just the flags:
80
+
81
+ ```python
82
+ {"is_vpn": True, "vpn": {"provider": "nordvpn"}} # one provider
83
+ {"is_resproxy": True, "resproxy": {"hits": {"gte": 5}}} # a numeric threshold
84
+ {"vpn": {"confidence": ["high", "medium"]}} # any of these
85
+ [{"is_tor": True}, {"is_resproxy": True}] # a list is OR
86
+ ```
87
+
88
+ Values are matched by equality, strings without regard to case. A list means any-of. A dict of `gte`/`gt`/`lte`/`lt` compares numbers, and every bound you give must hold, so two of them are a range. Members set to `False` or `None` are ignored, so a condition states the signals you act on; one that constrains nothing would match every request, and is refused when the middleware is built rather than silently blocking all your traffic.
89
+
90
+ Replace the refusal with `on_blocked`.
91
+
92
+ ## Where the client address comes from
93
+
94
+ This is the setting that decides whether any of the above works, and it is the one thing only you can get right.
95
+
96
+ By default the extension uses `request.remote_addr`, which is the socket peer unless you have wrapped the app in werkzeug's `ProxyFix`. Behind a load balancer without it, every visitor looks like the load balancer — a datacenter address, so a hosting rule would block all of them. Adding `ProxyFix` is Flask's own answer and everything else here follows from it.
97
+
98
+ For an edge that writes the address into its own header, name the header:
99
+
100
+ ```python
101
+ from python_flask import header_ip_selector
102
+
103
+ ip_selector = header_ip_selector("CF-Connecting-IP") # or True-Client-IP, or your own
104
+ ```
105
+
106
+ `xff_ip_selector()` reads `X-Forwarded-For` directly. Be aware that the left-most entry is whatever the caller sent, because proxies append to that header — it is only trustworthy when an edge you control overwrites it. If you know how many proxies sit in front, count from the right instead: `xff_ip_selector(1)` is the address your nearest proxy saw.
107
+
108
+ Anything else, pass your own callable. It receives the request and returns an address.
109
+
110
+ If the address resolves to a private one, the middleware says so once through the `vpndetection` logger. That is expected on localhost and is the signal to fix your configuration anywhere else.
111
+
112
+ ## When a lookup fails
113
+
114
+ The request is let through, and the reason is recorded on the answer's `error`. Our outage should not become yours, so a network failure, an exhausted quota or a rejected key all fail open. Pass `fail_closed=True` to block instead. Private addresses are answered locally and never fail, so this will not lock you out in development.
115
+
116
+ ## Cost and latency
117
+
118
+ Answers are cached for an hour, so a returning visitor costs nothing, and private addresses never leave the process. A cache miss is one request to our API, bounded at 2.5 seconds by default and not retried — on a request path, failing open quickly beats holding a visitor while we try again. Both are adjustable, as is the cache, through a `vpndetection` client you build yourself and pass as `client`.
119
+
120
+ Skip what you do not care about:
121
+
122
+ ```python
123
+ skip = lambda request: request.path.startswith("/static")
124
+ ```
125
+
126
+ Beyond a few million distinct visitors a day, stop calling the API per request: [download the dataset](https://vpndetection.io/databases) and look addresses up locally instead.
127
+
128
+ ## Absent is not false
129
+
130
+ Only `ip` and `is_vpn` come back on every plan. A field your plan does not include is `None`, which means "not in your plan" rather than "checked, and no".
131
+
132
+ ```python
133
+ lookup().result.is_hosting # None when your plan does not include it
134
+ ```
135
+
136
+ A `block_condition` naming a member your plan does not serve can never match, so the middleware warns once instead of failing silently. Pass `on_missing_field="raise"` to make it an error.
137
+
138
+ ## Other Libraries
139
+
140
+ There are official VPNDetection client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/vpndetection-io for more.
141
+
142
+ ## About VPNDetection
143
+
144
+ VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.
145
+
146
+ [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)
147
+
148
+ ## License
149
+
150
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,121 @@
1
+ # [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Flask Extension
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/vpndetection-flask.svg)](https://pypi.org/project/vpndetection-flask/)
4
+ [![license](https://img.shields.io/pypi/l/vpndetection-flask.svg)](LICENSE)
5
+
6
+ The official Flask Extension for the [VPNDetection](https://vpndetection.io) API.
7
+
8
+ It classifies the visitor behind each request — VPN, residential proxy, Tor, hosting, CDN, relay — and hands the answer to your code. Blocking is opt-in.
9
+
10
+ ## Getting Started
11
+
12
+ ```bash
13
+ pip install vpndetection-flask
14
+ ```
15
+
16
+ Requires Python 3.11 or newer.
17
+
18
+ You need an API key. Create one in the [console](https://app.vpndetection.io); the free tier's allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
19
+
20
+ ```python
21
+ from flask import Flask
22
+ from vpndetection_flask import VPNDetection
23
+
24
+ app = Flask(__name__)
25
+ VPNDetection(app, api_key=os.environ["VPNDETECTION_API_KEY"])
26
+ ```
27
+
28
+ `init_app(app)` is supported for the application-factory pattern.
29
+
30
+ ```python
31
+ from vpndetection_flask import lookup
32
+
33
+
34
+ @app.route("/")
35
+ def index():
36
+ found = lookup()
37
+ return "Hello, VPN user" if found.result.is_vpn else "Hello"
38
+ ```
39
+
40
+ By default nothing is blocked. Every request gets an answer and your own code decides what that means — which is usually what you want, because whether a VPN visitor is a problem depends entirely on what they are doing.
41
+
42
+ ## Blocking
43
+
44
+ Pass a `block_condition` and a matching request is answered with `403` and never reaches your code.
45
+
46
+ ```python
47
+ block_condition = {"is_vpn": True}
48
+ ```
49
+
50
+ A condition is written in the shape of a result, keyed by the same names the API uses, and only the members you name are considered. That lets it reach the evidence, not just the flags:
51
+
52
+ ```python
53
+ {"is_vpn": True, "vpn": {"provider": "nordvpn"}} # one provider
54
+ {"is_resproxy": True, "resproxy": {"hits": {"gte": 5}}} # a numeric threshold
55
+ {"vpn": {"confidence": ["high", "medium"]}} # any of these
56
+ [{"is_tor": True}, {"is_resproxy": True}] # a list is OR
57
+ ```
58
+
59
+ Values are matched by equality, strings without regard to case. A list means any-of. A dict of `gte`/`gt`/`lte`/`lt` compares numbers, and every bound you give must hold, so two of them are a range. Members set to `False` or `None` are ignored, so a condition states the signals you act on; one that constrains nothing would match every request, and is refused when the middleware is built rather than silently blocking all your traffic.
60
+
61
+ Replace the refusal with `on_blocked`.
62
+
63
+ ## Where the client address comes from
64
+
65
+ This is the setting that decides whether any of the above works, and it is the one thing only you can get right.
66
+
67
+ By default the extension uses `request.remote_addr`, which is the socket peer unless you have wrapped the app in werkzeug's `ProxyFix`. Behind a load balancer without it, every visitor looks like the load balancer — a datacenter address, so a hosting rule would block all of them. Adding `ProxyFix` is Flask's own answer and everything else here follows from it.
68
+
69
+ For an edge that writes the address into its own header, name the header:
70
+
71
+ ```python
72
+ from python_flask import header_ip_selector
73
+
74
+ ip_selector = header_ip_selector("CF-Connecting-IP") # or True-Client-IP, or your own
75
+ ```
76
+
77
+ `xff_ip_selector()` reads `X-Forwarded-For` directly. Be aware that the left-most entry is whatever the caller sent, because proxies append to that header — it is only trustworthy when an edge you control overwrites it. If you know how many proxies sit in front, count from the right instead: `xff_ip_selector(1)` is the address your nearest proxy saw.
78
+
79
+ Anything else, pass your own callable. It receives the request and returns an address.
80
+
81
+ If the address resolves to a private one, the middleware says so once through the `vpndetection` logger. That is expected on localhost and is the signal to fix your configuration anywhere else.
82
+
83
+ ## When a lookup fails
84
+
85
+ The request is let through, and the reason is recorded on the answer's `error`. Our outage should not become yours, so a network failure, an exhausted quota or a rejected key all fail open. Pass `fail_closed=True` to block instead. Private addresses are answered locally and never fail, so this will not lock you out in development.
86
+
87
+ ## Cost and latency
88
+
89
+ Answers are cached for an hour, so a returning visitor costs nothing, and private addresses never leave the process. A cache miss is one request to our API, bounded at 2.5 seconds by default and not retried — on a request path, failing open quickly beats holding a visitor while we try again. Both are adjustable, as is the cache, through a `vpndetection` client you build yourself and pass as `client`.
90
+
91
+ Skip what you do not care about:
92
+
93
+ ```python
94
+ skip = lambda request: request.path.startswith("/static")
95
+ ```
96
+
97
+ Beyond a few million distinct visitors a day, stop calling the API per request: [download the dataset](https://vpndetection.io/databases) and look addresses up locally instead.
98
+
99
+ ## Absent is not false
100
+
101
+ Only `ip` and `is_vpn` come back on every plan. A field your plan does not include is `None`, which means "not in your plan" rather than "checked, and no".
102
+
103
+ ```python
104
+ lookup().result.is_hosting # None when your plan does not include it
105
+ ```
106
+
107
+ A `block_condition` naming a member your plan does not serve can never match, so the middleware warns once instead of failing silently. Pass `on_missing_field="raise"` to make it an error.
108
+
109
+ ## Other Libraries
110
+
111
+ There are official VPNDetection client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/vpndetection-io for more.
112
+
113
+ ## About VPNDetection
114
+
115
+ VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.
116
+
117
+ [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)
118
+
119
+ ## License
120
+
121
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "vpndetection-flask"
7
+ dynamic = ["version"]
8
+ description = "Official Flask extension for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN visitors on every request."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Mslm Dev", email = "support@vpndetection.io" }]
14
+ keywords = ["vpn", "vpn-detection", "proxy-detection", "tor", "ip-intelligence", "fraud-prevention", "middleware"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Internet",
24
+ "Topic :: Security",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = ["vpndetection>=2.1.0,<4", "flask>=3.0"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://vpndetection.io"
31
+ Source = "https://github.com/vpndetection-io/sdk-python-flask"
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=8", "mypy>=1.11", "ruff>=0.6"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/vpndetection_flask"]
38
+
39
+ [tool.hatch.version]
40
+ path = "src/vpndetection_flask/__init__.py"
41
+
42
+ [tool.ruff]
43
+ line-length = 96
44
+
45
+ [tool.mypy]
46
+ python_version = "3.11"
47
+ strict = true
@@ -0,0 +1,123 @@
1
+ """Official Flask extension for the VPNDetection API.
2
+
3
+ Classifies the visitor behind each request and hangs the answer off ``g.vpndetection``,
4
+ where your views can read it. Blocking is opt-in.
5
+
6
+ from flask import Flask
7
+ from vpndetection_flask import VPNDetection
8
+
9
+ app = Flask(__name__)
10
+ VPNDetection(app, api_key=os.environ["VPNDETECTION_API_KEY"])
11
+
12
+ The framework-agnostic half lives in ``vpndetection.middleware``; this package is only
13
+ the parts that are genuinely Flask-shaped.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable
19
+ from typing import Any
20
+
21
+ from flask import Flask, Response, g, jsonify, request
22
+ from vpndetection.middleware import (
23
+ Core,
24
+ IpSelector,
25
+ Lookup,
26
+ Options,
27
+ RequestView,
28
+ Selectors,
29
+ bind_selectors,
30
+ )
31
+ from werkzeug.wrappers import Request as WerkzeugRequest
32
+
33
+ __all__ = [
34
+ "VPNDetection",
35
+ "default_ip_selector",
36
+ "header_ip_selector",
37
+ "lookup",
38
+ "xff_ip_selector",
39
+ ]
40
+
41
+ __version__ = "1.0.0"
42
+
43
+ _SELECTORS: Selectors[WerkzeugRequest] = bind_selectors(
44
+ lambda req: RequestView(
45
+ header=lambda name: req.headers.get(name),
46
+ framework_ip=lambda: req.remote_addr,
47
+ )
48
+ )
49
+
50
+ #: ``request.remote_addr``, which is the socket peer unless you have wrapped the app in
51
+ #: werkzeug's ``ProxyFix``.
52
+ #:
53
+ #: Behind a load balancer without it, every visitor looks like the load balancer - a
54
+ #: datacenter address a hosting rule would block them all for. If you are behind one,
55
+ #: add ``ProxyFix`` (Flask's own answer) or use :func:`header_ip_selector`.
56
+ default_ip_selector: IpSelector[WerkzeugRequest] = _SELECTORS.default
57
+
58
+ #: An address from ``X-Forwarded-For``.
59
+ #:
60
+ #: The LEFT-MOST entry (``depth`` 0) is whatever the caller sent, because proxies
61
+ #: append to this header. It is only trustworthy when an edge you control overwrites
62
+ #: it. When you know how many proxies sit in front, count from the right:
63
+ #: ``xff_ip_selector(1)`` is the address your nearest proxy saw.
64
+ xff_ip_selector = _SELECTORS.xff
65
+
66
+ #: An address from a single-value header your edge writes -
67
+ #: ``header_ip_selector("CF-Connecting-IP")`` behind Cloudflare. Falls back to
68
+ #: ``remote_addr`` when the header is absent.
69
+ header_ip_selector = _SELECTORS.header
70
+
71
+
72
+ def lookup() -> Lookup | None:
73
+ """What the extension found out about this visitor.
74
+
75
+ None when the extension has not run for this request, or when ``skip`` claimed it.
76
+ """
77
+ return getattr(g, "vpndetection", None)
78
+
79
+
80
+ class VPNDetection:
81
+ """Classify the visitor, and optionally refuse the request.
82
+
83
+ Takes everything :class:`vpndetection.middleware.Options` does, plus ``on_blocked``.
84
+
85
+ Without a ``block_condition`` this only enriches the request and never refuses one,
86
+ leaving the decision to your own views. A lookup that fails - network, quota, an
87
+ outage of ours - lets the request through and records why on ``lookup().error``,
88
+ unless you set ``fail_closed``.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ app: Flask | None = None,
94
+ *,
95
+ on_blocked: Callable[[Lookup], Response] | None = None,
96
+ **options: Any,
97
+ ) -> None:
98
+ self._on_blocked = on_blocked or _refuse
99
+ self._core: Core[WerkzeugRequest] = Core(Options(**options), default_ip_selector)
100
+ if app is not None:
101
+ self.init_app(app)
102
+
103
+ def init_app(self, app: Flask) -> None:
104
+ """Register on an app, for the factory pattern."""
105
+ app.extensions.setdefault("vpndetection", self)
106
+ app.before_request(self._before_request)
107
+
108
+ def _before_request(self) -> Response | None:
109
+ found = self._core.evaluate(request)
110
+ if found is None:
111
+ return None
112
+ g.vpndetection = found
113
+ if found.blocked:
114
+ # Returning a response from before_request is what STOPS the view from
115
+ # running; merely building one would let the request through.
116
+ return self._on_blocked(found)
117
+ return None
118
+
119
+
120
+ def _refuse(_lookup: Lookup) -> Response:
121
+ response = jsonify({"error": "access denied"})
122
+ response.status_code = 403
123
+ return response
@@ -0,0 +1,60 @@
1
+ {
2
+ "v4": [
3
+ "0.0.0.0/8",
4
+ "10.0.0.0/8",
5
+ "100.64.0.0/10",
6
+ "127.0.0.0/8",
7
+ "169.254.0.0/16",
8
+ "172.16.0.0/12",
9
+ "192.0.0.0/24",
10
+ "192.0.2.0/24",
11
+ "192.168.0.0/16",
12
+ "198.18.0.0/15",
13
+ "198.51.100.0/24",
14
+ "203.0.113.0/24",
15
+ "224.0.0.0/4",
16
+ "240.0.0.0/4",
17
+ "255.255.255.255/32"
18
+ ],
19
+ "v6": [
20
+ "::/128",
21
+ "::1/128",
22
+ "::ffff:0:0/96",
23
+ "::/96",
24
+ "100::/64",
25
+ "2001:10::/28",
26
+ "2001:db8::/32",
27
+ "fc00::/7",
28
+ "fe80::/10",
29
+ "fec0::/10",
30
+ "ff00::/8",
31
+ "2002::/24",
32
+ "2002:a00::/24",
33
+ "2002:7f00::/24",
34
+ "2002:a9fe::/32",
35
+ "2002:ac10::/28",
36
+ "2002:c000::/40",
37
+ "2002:c000:200::/40",
38
+ "2002:c0a8::/32",
39
+ "2002:c612::/31",
40
+ "2002:c633:6400::/40",
41
+ "2002:cb00:7100::/40",
42
+ "2002:e000::/20",
43
+ "2002:f000::/20",
44
+ "2002:ffff:ffff::/48",
45
+ "2001::/40",
46
+ "2001:0:a00::/40",
47
+ "2001:0:7f00::/40",
48
+ "2001:0:a9fe::/48",
49
+ "2001:0:ac10::/44",
50
+ "2001:0:c000::/56",
51
+ "2001:0:c000:200::/56",
52
+ "2001:0:c0a8::/48",
53
+ "2001:0:c612::/47",
54
+ "2001:0:c633:6400::/56",
55
+ "2001:0:cb00:7100::/56",
56
+ "2001:0:e000::/36",
57
+ "2001:0:f000::/36",
58
+ "2001:0:ffff:ffff::/64"
59
+ ]
60
+ }