howboutno 0.1.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,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sudeep-alt
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,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: howboutno
3
+ Version: 0.1.0
4
+ Summary: An open source ASGI middleware designed to block unwanted traffic on web apps.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: beartype>=0.22.9
8
+ Requires-Dist: cachetools>=6.2.6
9
+ Requires-Dist: httpx>=0.28.1
10
+ Description-Content-Type: text/markdown
11
+
12
+ # HowAboutNo
13
+ *Say no to unwanted traffic*
14
+
15
+ **HowAboutNo** is an open source ASGI middleware designed to block unwanted traffic on web apps. Supports all ASGI frameworks.
16
+
17
+ **Version:** `0.1.0`
18
+
19
+ ## Features
20
+ - Block traffic based on IP, continent, country, ASN, RDNS hostname, hosting and proxy status.
21
+ - Customizable responses for blocked traffic.
22
+ - Easy integration with any ASGI framework.
23
+ - Simple configuration using a TOML file.
24
+ - Lightweight and efficient implementation.
25
+ - Reliable IP lookups using [ip-api.com](http://ip-api.com/).
26
+ - Caching of IP lookup results to minimize latency and avoid hitting rate limits.
27
+ - Configurable cache invalidation times for successful and unsuccessful entries.
28
+ - Exception lists to allow certain IPs and paths to bypass blocking rules.
29
+ - Optional logging of blocked requests.
30
+
31
+ ## Installation
32
+ ```bash
33
+ pip install howaboutno
34
+ ```
35
+ or whatever method you prefer to install Python packages.
36
+
37
+ ## Usage
38
+ 1. Create a configuration file (e.g., `config.toml`) either manually or by runnning the `howaboutno` command, which generates a sample config file in the current working directory.
39
+ 2. Wrap your ASGI app with the `HowAboutNo` middleware, passing the path to your configuration file.
40
+
41
+ That's it. HowAboutNo will now block unwanted traffic based on the rules defined in your configuration file.
42
+
43
+ ### Configuration
44
+ The configuration file is in TOML format and supports the following options:
45
+ - `block_ip`: List of IP addresses to block.
46
+ - `block_continent`: List of continent codes to block.
47
+ - `block_country`: List of country codes to block.
48
+ - `block_asn`: List of ASNs to block.
49
+ - `block_rdns_hostname`: List of RDNS hostnames to block.
50
+ - `allow_hosting`: Whether to block hosting providers.
51
+ - `allow_proxy`: Whether to block proxies.
52
+ - `exception_ip`: List of IP addresses to exclude from blocking.
53
+ - `exception_path`: List of URL paths to exclude from blocking.
54
+ - `response`: Custom responses for different block types.
55
+ - `cache`: Cache settings, including size and invalidation times.
56
+ - `disable_logging`: Option to disable logging of blocked requests.
57
+
58
+
59
+ ### Detailed config example
60
+ ```toml
61
+ # Block IPs (both IPv4 and IPv6 supported)
62
+ [block_ip]
63
+ block_ip = ["1.1.1.1", "2.2.2.2"]
64
+
65
+ # Block bad IPs from public blocklists (fetched at app startup)
66
+ [block_bad_ip]
67
+ block_inbound_bad_ip = true
68
+ block_outbound_bad_ip = true
69
+
70
+ # Block continents (2-letter continent codes)
71
+ [block_continent]
72
+ block_continent = ["AS", "EU"]
73
+
74
+ # Block countries (ISO 3166-1 alpha-2 country codes)
75
+ [block_country]
76
+ block_country = ["IN", "CN"]
77
+
78
+ # Block ASNs (Autonomous System Numbers)
79
+ [block_asn]
80
+ block_asn = [12345, 67890]
81
+
82
+ # Block reverse DNS hostnames
83
+ [block_rdns_hostname]
84
+ block_rdns_hostname = ["badhost.example.com", "malicious.example.net"]
85
+
86
+ # Allow hosting providers (true/false)
87
+ [allow_hosting]
88
+ allow_hosting = false
89
+
90
+ # Allow proxies (true/false)
91
+ [allow_proxy]
92
+ allow_proxy = false
93
+
94
+ # Exception IPs (list of IP addresses to exclude from blocking)
95
+ [exception_ip]
96
+ exception_ip = ["3.3.3.3"]
97
+
98
+ # Exception paths (list of URL paths to exclude from blocking)
99
+ [exception_path]
100
+ exception_path = ["/health", "/status"]
101
+
102
+ # Custom responses for different block types
103
+
104
+ # Response for IP blocks
105
+ [response.ip]
106
+ # The response string can be a JSON string, HTML string, or plain text string based on the specified return type.
107
+ response = "{\"detail\": \"Access denied due to your IP address.\"}"
108
+ # The status code to return when a block is triggered.
109
+ status_code = 403
110
+ # The return_as field specifies the format of the response. The supported values are "JSON", "HTML", and "TEXT". Case-insensitive.
111
+ return_as = "JSON"
112
+
113
+ # Similarly, you can define custom responses for continent, country, ASN, rdns_hostname, hosting, and proxy blocks.
114
+
115
+ # Response for all blocks (overrides specific block responses if defined)
116
+ [response.all]
117
+ response = "<h1>Access Denied</h1><p>Your request has been blocked.</p>"
118
+ status_code = 403
119
+ return_as = "HTML"
120
+
121
+ # Cache settings
122
+
123
+ [cache]
124
+ # Maximum number of entries in the cache
125
+ size = 512
126
+ # Time in seconds after which a successful cache entry should be invalidated (default: 7 days)
127
+ invalidate_success_after = 604800
128
+ # Time in seconds after which an error cache entry should be invalidated (default: 1 hour)
129
+ invalidate_error_after = 3600
130
+
131
+ # Disable logging (true/false)
132
+ [disable_logging]
133
+ disable_logging = false
134
+ ```
135
+
136
+ ## Implementation
137
+ ### FastAPI
138
+ ```python
139
+ from fastapi import FastAPI
140
+ from howaboutno import HowAboutNo
141
+
142
+ app = FastAPI()
143
+
144
+ @app.get("/")
145
+ def root():
146
+ return {
147
+ "message": "Hello, world!"
148
+ }
149
+
150
+ # Must be at the very end after all routes have been defined and after all other middleware have been added if there are any.
151
+ app = HowAboutNo(app, config="config.toml")
152
+ ```
153
+
154
+ > [!NOTE]
155
+ Wrapping the app with HowAboutNo at the end instead of using `add_middleware` is recommended since Starlette, which FastAPI is built on top of, suppresses exceptions raised in middleware's initialization when using `add_middleware`, and only raised when a request is made, which can make debugging difficult. Wrapping the app with HowAboutNo directly will ensure that HowAboutNo is the outermost layer of the app and any exceptions raised in its initialization will be raised immediately, making them easier to debug.
156
+
157
+ ### Starlette
158
+ ```python
159
+ from starlette.applications import Starlette
160
+ from starlette.responses import JSONResponse
161
+ from starlette.requests import Request
162
+ from starlette.routing import Route
163
+ from howaboutno import HowAboutNo
164
+
165
+ async def root(request: Request):
166
+ return JSONResponse({"message": "Hello, world!"})
167
+
168
+ routes = [
169
+ Route("/", root)
170
+ ]
171
+
172
+ app = Starlette(routes=routes)
173
+
174
+ # Must be at the very end after all routes have been defined and after all other middleware have been added if there are any.
175
+ app = HowAboutNo(app, config="config.toml")
176
+ ```
177
+
178
+ > [!NOTE]
179
+ Similarly to FastAPI, wrapping the app with HowAboutNo at the end instead of using Starlette's `Middleware` class is recommended to ensure that any exceptions raised in the initialization of HowAboutNo are raised immediately, making them easier to debug.
180
+
181
+ ### Pure ASGI
182
+ ```python
183
+ from howaboutno import HowAboutNo
184
+
185
+ async def app(scope, receive, send):
186
+ if scope["type"] == "http":
187
+ await send({
188
+ "type": "http.response.start",
189
+ "status": 200,
190
+ "headers": [
191
+ [b"content-type", b"text/plain"],
192
+ ],
193
+ })
194
+
195
+ await send({
196
+ "type": "http.response.body",
197
+ "body": b"Hello, world!",
198
+ })
199
+
200
+ # Must be at the very end after all routes have been defined and after all other middleware have been added if there are any.
201
+ app = HowAboutNo(app, config="config.toml")
202
+
203
+
204
+ ### Other ASGI frameworks
205
+ HowAboutNo can be used with any ASGI framework by wrapping the ASGI app with HowAboutNo and passing the path to the configuration file after all routes and other middleware have been defined.
206
+
207
+ ## Architecture
208
+ ### IP retrieval
209
+ The client IP is retrieved from `scope["client"][0]`, the middleware assumes the client key always exists. Additionally, it assumes `scope["client"][0]` is a valid IP address.
210
+
211
+ ### Request blocking
212
+ A request is blocked if **any** of the following match (checked top → bottom):
213
+
214
+ - IP is in `block_ip` and absent from `exception_ip` and path is absent from `exception_path`
215
+ - `block_inbound_bad_ip` is true and IP is in the inbound bad IP blocklist and IP is absent from `exception_ip` and path is absent from `exception_path`
216
+ - `block_outbound_bad_ip` is true and IP is in the outbound bad IP blocklist and IP is absent from `exception_ip` and path is absent from `exception_path`
217
+ - Continent is in `block_continent` and absent from `exception_ip` and path is absent from `exception_path`
218
+ - Country is in `block_country` and absent from `exception_ip` and path is absent from `exception_path`
219
+ - ASN is in `block_asn` and absent from `exception_ip` and path is absent from `exception_path`
220
+ - rDNS hostname is in `block_rdns_hostname` and absent from `exception_ip` and path is absent from `exception_path`
221
+ - Hosting IP while `allow_hosting = false` and IP is absent from `exception_ip` and path is absent from `exception_path`
222
+ - Proxy IP while `allow_proxy = false` and IP is absent from `exception_ip` and path is absent from `exception_path`
223
+
224
+ **Rule**
225
+ - Only the response for the *first matching block condition* is returned.
226
+
227
+ **Example**
228
+ - If an IP matches both `block_country` and `block_asn`
229
+ - The `block_country` response is used
230
+
231
+ ### Data source
232
+ HowAboutNo uses [ip-api.com](http://ip-api.com/) for IP lookups, which provides data on continent, country, ASN, rDNS hostname, hosting and proxy status for a given IP address.
233
+ > [!NOTE]
234
+ The data returned by ip-api.com may contain errors or be inaccurate. HowAboutNo does not make any guarantees regarding the accuracy of the data returned by ip-api.com and is not responsible for any consequences that may arise from blocking decisions based on this data.
235
+ The middleware returns a 503 if it fails to lookup the data of the IP address.
236
+
237
+ HowAboutNo also inherits the limitations of ip-api.com, including potential inaccuracies in the data and rate limits on requests. Enabling caching can help mitigate the impact of rate limits, but it's important to be aware of these limitations when using HowAboutNo.
238
+
239
+ > [!WARNING]
240
+ HowAboutNo relies on ip-api.com for its blocking functionality, and ip-api.com has its own terms of service which must be followed. Make sure to review ip-api.com's terms of service before using HowAboutNo, especially if you plan to use it for commercial purposes.
241
+
242
+ ### Caching
243
+ HowAboutNo uses an LRU cache to store IP lookup results from ip-api.com.
244
+
245
+ To disable caching, set `size` to `0`.
246
+ To disable cache invalidation, set `invalidate_success_after` or `invalidate_error_after` to `0` according to your needs.
247
+
248
+ - **When caching and invalidation are enabled:**
249
+ - On the first request from an IP address, its data is looked up and cached (with eviction if the cache is full).
250
+ - On subsequent requests from the same IP:
251
+ - If the cache entry is successful (status code 200) and time elapsed since caching exceeds `invalidate_success_after`, or if the cache entry is an error (non-200 status code) and time elapsed since caching exceeds `invalidate_error_after`, the IP is looked up again and the cache entry is refreshed.
252
+ - Otherwise, the cached data is returned.
253
+
254
+ - **When caching is enabled but invalidation is disabled:**
255
+ - On the first request from an IP address, its data is looked up and cached, evicting older entries if necessary.
256
+ - On subsequent requests from the same IP, the cached data is returned indefinitely until it is evicted due to cache size limits.
257
+ - This applies individually to successful and error entries based on the `invalidate_success_after` and `invalidate_error_after` settings.
258
+
259
+ - **When caching is disabled:**
260
+ - The IP address is looked up on every request.
261
+
262
+ ## FAQ
263
+ ### General
264
+ - **Q: Will this middleware work with any ASGI framework?**
265
+ - A: Yes, HowAboutNo is built to be compatible with any ASGI framework. You can wrap any ASGI app with HowAboutNo to add the blocking functionality.
266
+ - **Q: Why can't I use Cloudflare or a similar service instead of this middleware?**
267
+ - A: To avoid corporate dependencies and have full control over the blocking logic and responses. HowAboutNo allows you to define custom blocking rules and responses without relying on third-party services, which can be important for privacy, security, and customization reasons.
268
+ - **Q: Is HowAboutNo free to use?**
269
+ - A: Yes, HowAboutNo is open source and free to use under the MIT License.
270
+ - **Q: Why ASGI middleware and not WSGI middleware?**
271
+ - A: To be honest, I don't have a specific reason for choosing ASGI over WSGI. I chose ASGI simply because it's the modern standard for Python web applications.
272
+ - **Q: Can I use HowAboutNo with a WSGI app?**
273
+ - A: Not directly, since HowAboutNo is designed as ASGI middleware. However, you can use an ASGI-to-WSGI adapter like `asgiref.wsgi.WsgiToAsgi` to wrap your WSGI app and then apply HowAboutNo as ASGI middleware to the wrapped app.
274
+ - **Q: Can I use this for commercial purposes?**
275
+ - A: Yes, but also no, you can use the source code for commercial purposes. However, the IP lookup functionality relies on [ip-api.com](http://ip-api.com/), which has its own terms of service which does not allow commercial use without a paid plan.
276
+ - **Q: Why such a casual name for a middleware?**
277
+ - A: Because I said so!
278
+ - **Q: Will I get a girlfriend by using this middleware?**
279
+ - A: Uh, maybe? Who knows!
280
+
281
+ ### Architecture
282
+ - **Q: Will my app be affected by [ip-api.com](https://ip-api.com/) rate limits?**
283
+ - A: Yes, but not in the way you might think. Caching is implemented to minimize the number of requests made to ip-api.com, which helps avoid hitting rate limits. However, if you do hit a rate limit, HowAboutNo will return a 503 Service Unavailable response for requests that require an IP lookup until the rate limit resets. This means that your app will not be able to perform IP lookups during this time, but it will still be able to serve requests that do not require IP lookups (e.g., requests from IPs that are already in the cache).
284
+ - **Q: What happens if ip-api.com is unreachable or returns an error?**
285
+ - A: If ip-api.com returns an error or is unreachable, HowAboutNo will treat the lookup as unsuccessful and will return a 503 Service Unavailable response.
286
+ - **Q: If multiple block conditions match for a request, which response is returned?**
287
+ - A: Only the response for the first matching block condition is returned. The block conditions are checked in the following order: IP, continent, country, ASN, rDNS hostname, hosting status, and proxy status. So if a request matches both `block_country` and `block_asn`, the response defined for `block_country` will be returned.
288
+ - **Q: Can I exclude certain IPs or paths from being blocked?**
289
+ - A: Yes, you can use the `exception_ip` and `exception_path` settings in the configuration file to specify IP addresses and URL paths that should be excluded from blocking rules. If a request matches a block condition but the IP is in `exception_ip` or the path is in `exception_path`, the request will not be blocked based on that condition.
290
+ - **Q: What happens if an IP address matches a block condition but is also in the exception list?**
291
+ - A: If an IP address matches a block condition but is also listed in `exception_ip`, the blocking rules will be bypassed for that IP address, and the request will not be blocked based on that condition. The same applies to URL paths listed in `exception_path`. This allows you to have granular control over which IPs and paths are subject to blocking rules.
292
+ - **Q: What happens if an IP address is present in both `block_ip` and `exception_ip`?**
293
+ - A: If an IP address is present in both `block_ip` and `exception_ip`, the blocking rules will be bypassed for that IP address, and the request will not be blocked based on the `block_ip` condition. The presence of the IP address in `exception_ip` takes precedence over its presence in `block_ip`.
294
+ - **Q: If caching is enabled and an IP address is blocked based on a cached entry, but the actual data for that IP has changed since it was cached, will the middleware still block the request?**
295
+ - A: Yes, if the cache entry for that IP address indicates that it should be blocked, the middleware will block the request based on the cached data. The cache invalidation settings determine how long entries remain in the cache before they are refreshed, but while they are in the cache, their data is used for blocking decisions.
296
+ - **Q: What do `invalidate_success_after` and `invalidate_error_after` do?**
297
+ - A: `invalidate_success_after` specifies the time in seconds after which a successful cache entry (status code 200) should be invalidated and refreshed on the next request. `invalidate_error_after` specifies the time in seconds after which an unsuccessful cache entry (non-200 status code) should be invalidated and refreshed on the next request. Setting either of these to `0` disables invalidation for that type of entry, meaning entries will remain in the cache indefinitely until evicted due to cache size limits.
298
+
299
+ ### Troubleshooting
300
+ - **Q: I'm seeing a lot of 503 Service Unavailable responses. What could be causing this?**
301
+ - A: This could be caused by several factors:
302
+ - You might be hitting the rate limits of ip-api.com, especially if caching is disabled or if you have a high volume of requests from unique IP addresses. In this case, you would need to wait until the rate limit resets.
303
+ - ip-api.com might be experiencing downtime or issues, which would cause lookups to fail and result in 503 responses. You can check the status of ip-api.com to see if there are any reported issues.
304
+ - There could be a network issue preventing your server from reaching ip-api.com, which would also lead to failed lookups and 503 responses. You can check your server's network connectivity to ensure it can reach ip-api.com.
305
+
306
+ - **Q: My configuration changes are not taking effect. What should I do?**
307
+ - A: The configuration file is read when the HowAboutNo middleware is initialized. If you make changes to the configuration file after the middleware has been initialized, those changes will not take effect until the middleware is reloaded. To apply configuration changes, you will need to restart your ASGI application so that the HowAboutNo middleware can read the updated configuration file.
308
+
309
+ Anything else? Please let me know by opening an issue!
310
+
311
+ ## LICENSE
312
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
313
+
314
+ ## Acknowledgements
315
+ - [ip-api.com](https://ip-api.com/) for providing the IP data.
316
+ - [bitwire-it](https://github.com/bitwire-it) for the [IP blocklist](https://github.com/bitwire-it/ipblocklist/) data.
317
+
318
+ If you found this project useful, please consider giving it a star!
@@ -0,0 +1,307 @@
1
+ # HowAboutNo
2
+ *Say no to unwanted traffic*
3
+
4
+ **HowAboutNo** is an open source ASGI middleware designed to block unwanted traffic on web apps. Supports all ASGI frameworks.
5
+
6
+ **Version:** `0.1.0`
7
+
8
+ ## Features
9
+ - Block traffic based on IP, continent, country, ASN, RDNS hostname, hosting and proxy status.
10
+ - Customizable responses for blocked traffic.
11
+ - Easy integration with any ASGI framework.
12
+ - Simple configuration using a TOML file.
13
+ - Lightweight and efficient implementation.
14
+ - Reliable IP lookups using [ip-api.com](http://ip-api.com/).
15
+ - Caching of IP lookup results to minimize latency and avoid hitting rate limits.
16
+ - Configurable cache invalidation times for successful and unsuccessful entries.
17
+ - Exception lists to allow certain IPs and paths to bypass blocking rules.
18
+ - Optional logging of blocked requests.
19
+
20
+ ## Installation
21
+ ```bash
22
+ pip install howaboutno
23
+ ```
24
+ or whatever method you prefer to install Python packages.
25
+
26
+ ## Usage
27
+ 1. Create a configuration file (e.g., `config.toml`) either manually or by runnning the `howaboutno` command, which generates a sample config file in the current working directory.
28
+ 2. Wrap your ASGI app with the `HowAboutNo` middleware, passing the path to your configuration file.
29
+
30
+ That's it. HowAboutNo will now block unwanted traffic based on the rules defined in your configuration file.
31
+
32
+ ### Configuration
33
+ The configuration file is in TOML format and supports the following options:
34
+ - `block_ip`: List of IP addresses to block.
35
+ - `block_continent`: List of continent codes to block.
36
+ - `block_country`: List of country codes to block.
37
+ - `block_asn`: List of ASNs to block.
38
+ - `block_rdns_hostname`: List of RDNS hostnames to block.
39
+ - `allow_hosting`: Whether to block hosting providers.
40
+ - `allow_proxy`: Whether to block proxies.
41
+ - `exception_ip`: List of IP addresses to exclude from blocking.
42
+ - `exception_path`: List of URL paths to exclude from blocking.
43
+ - `response`: Custom responses for different block types.
44
+ - `cache`: Cache settings, including size and invalidation times.
45
+ - `disable_logging`: Option to disable logging of blocked requests.
46
+
47
+
48
+ ### Detailed config example
49
+ ```toml
50
+ # Block IPs (both IPv4 and IPv6 supported)
51
+ [block_ip]
52
+ block_ip = ["1.1.1.1", "2.2.2.2"]
53
+
54
+ # Block bad IPs from public blocklists (fetched at app startup)
55
+ [block_bad_ip]
56
+ block_inbound_bad_ip = true
57
+ block_outbound_bad_ip = true
58
+
59
+ # Block continents (2-letter continent codes)
60
+ [block_continent]
61
+ block_continent = ["AS", "EU"]
62
+
63
+ # Block countries (ISO 3166-1 alpha-2 country codes)
64
+ [block_country]
65
+ block_country = ["IN", "CN"]
66
+
67
+ # Block ASNs (Autonomous System Numbers)
68
+ [block_asn]
69
+ block_asn = [12345, 67890]
70
+
71
+ # Block reverse DNS hostnames
72
+ [block_rdns_hostname]
73
+ block_rdns_hostname = ["badhost.example.com", "malicious.example.net"]
74
+
75
+ # Allow hosting providers (true/false)
76
+ [allow_hosting]
77
+ allow_hosting = false
78
+
79
+ # Allow proxies (true/false)
80
+ [allow_proxy]
81
+ allow_proxy = false
82
+
83
+ # Exception IPs (list of IP addresses to exclude from blocking)
84
+ [exception_ip]
85
+ exception_ip = ["3.3.3.3"]
86
+
87
+ # Exception paths (list of URL paths to exclude from blocking)
88
+ [exception_path]
89
+ exception_path = ["/health", "/status"]
90
+
91
+ # Custom responses for different block types
92
+
93
+ # Response for IP blocks
94
+ [response.ip]
95
+ # The response string can be a JSON string, HTML string, or plain text string based on the specified return type.
96
+ response = "{\"detail\": \"Access denied due to your IP address.\"}"
97
+ # The status code to return when a block is triggered.
98
+ status_code = 403
99
+ # The return_as field specifies the format of the response. The supported values are "JSON", "HTML", and "TEXT". Case-insensitive.
100
+ return_as = "JSON"
101
+
102
+ # Similarly, you can define custom responses for continent, country, ASN, rdns_hostname, hosting, and proxy blocks.
103
+
104
+ # Response for all blocks (overrides specific block responses if defined)
105
+ [response.all]
106
+ response = "<h1>Access Denied</h1><p>Your request has been blocked.</p>"
107
+ status_code = 403
108
+ return_as = "HTML"
109
+
110
+ # Cache settings
111
+
112
+ [cache]
113
+ # Maximum number of entries in the cache
114
+ size = 512
115
+ # Time in seconds after which a successful cache entry should be invalidated (default: 7 days)
116
+ invalidate_success_after = 604800
117
+ # Time in seconds after which an error cache entry should be invalidated (default: 1 hour)
118
+ invalidate_error_after = 3600
119
+
120
+ # Disable logging (true/false)
121
+ [disable_logging]
122
+ disable_logging = false
123
+ ```
124
+
125
+ ## Implementation
126
+ ### FastAPI
127
+ ```python
128
+ from fastapi import FastAPI
129
+ from howaboutno import HowAboutNo
130
+
131
+ app = FastAPI()
132
+
133
+ @app.get("/")
134
+ def root():
135
+ return {
136
+ "message": "Hello, world!"
137
+ }
138
+
139
+ # Must be at the very end after all routes have been defined and after all other middleware have been added if there are any.
140
+ app = HowAboutNo(app, config="config.toml")
141
+ ```
142
+
143
+ > [!NOTE]
144
+ Wrapping the app with HowAboutNo at the end instead of using `add_middleware` is recommended since Starlette, which FastAPI is built on top of, suppresses exceptions raised in middleware's initialization when using `add_middleware`, and only raised when a request is made, which can make debugging difficult. Wrapping the app with HowAboutNo directly will ensure that HowAboutNo is the outermost layer of the app and any exceptions raised in its initialization will be raised immediately, making them easier to debug.
145
+
146
+ ### Starlette
147
+ ```python
148
+ from starlette.applications import Starlette
149
+ from starlette.responses import JSONResponse
150
+ from starlette.requests import Request
151
+ from starlette.routing import Route
152
+ from howaboutno import HowAboutNo
153
+
154
+ async def root(request: Request):
155
+ return JSONResponse({"message": "Hello, world!"})
156
+
157
+ routes = [
158
+ Route("/", root)
159
+ ]
160
+
161
+ app = Starlette(routes=routes)
162
+
163
+ # Must be at the very end after all routes have been defined and after all other middleware have been added if there are any.
164
+ app = HowAboutNo(app, config="config.toml")
165
+ ```
166
+
167
+ > [!NOTE]
168
+ Similarly to FastAPI, wrapping the app with HowAboutNo at the end instead of using Starlette's `Middleware` class is recommended to ensure that any exceptions raised in the initialization of HowAboutNo are raised immediately, making them easier to debug.
169
+
170
+ ### Pure ASGI
171
+ ```python
172
+ from howaboutno import HowAboutNo
173
+
174
+ async def app(scope, receive, send):
175
+ if scope["type"] == "http":
176
+ await send({
177
+ "type": "http.response.start",
178
+ "status": 200,
179
+ "headers": [
180
+ [b"content-type", b"text/plain"],
181
+ ],
182
+ })
183
+
184
+ await send({
185
+ "type": "http.response.body",
186
+ "body": b"Hello, world!",
187
+ })
188
+
189
+ # Must be at the very end after all routes have been defined and after all other middleware have been added if there are any.
190
+ app = HowAboutNo(app, config="config.toml")
191
+
192
+
193
+ ### Other ASGI frameworks
194
+ HowAboutNo can be used with any ASGI framework by wrapping the ASGI app with HowAboutNo and passing the path to the configuration file after all routes and other middleware have been defined.
195
+
196
+ ## Architecture
197
+ ### IP retrieval
198
+ The client IP is retrieved from `scope["client"][0]`, the middleware assumes the client key always exists. Additionally, it assumes `scope["client"][0]` is a valid IP address.
199
+
200
+ ### Request blocking
201
+ A request is blocked if **any** of the following match (checked top → bottom):
202
+
203
+ - IP is in `block_ip` and absent from `exception_ip` and path is absent from `exception_path`
204
+ - `block_inbound_bad_ip` is true and IP is in the inbound bad IP blocklist and IP is absent from `exception_ip` and path is absent from `exception_path`
205
+ - `block_outbound_bad_ip` is true and IP is in the outbound bad IP blocklist and IP is absent from `exception_ip` and path is absent from `exception_path`
206
+ - Continent is in `block_continent` and absent from `exception_ip` and path is absent from `exception_path`
207
+ - Country is in `block_country` and absent from `exception_ip` and path is absent from `exception_path`
208
+ - ASN is in `block_asn` and absent from `exception_ip` and path is absent from `exception_path`
209
+ - rDNS hostname is in `block_rdns_hostname` and absent from `exception_ip` and path is absent from `exception_path`
210
+ - Hosting IP while `allow_hosting = false` and IP is absent from `exception_ip` and path is absent from `exception_path`
211
+ - Proxy IP while `allow_proxy = false` and IP is absent from `exception_ip` and path is absent from `exception_path`
212
+
213
+ **Rule**
214
+ - Only the response for the *first matching block condition* is returned.
215
+
216
+ **Example**
217
+ - If an IP matches both `block_country` and `block_asn`
218
+ - The `block_country` response is used
219
+
220
+ ### Data source
221
+ HowAboutNo uses [ip-api.com](http://ip-api.com/) for IP lookups, which provides data on continent, country, ASN, rDNS hostname, hosting and proxy status for a given IP address.
222
+ > [!NOTE]
223
+ The data returned by ip-api.com may contain errors or be inaccurate. HowAboutNo does not make any guarantees regarding the accuracy of the data returned by ip-api.com and is not responsible for any consequences that may arise from blocking decisions based on this data.
224
+ The middleware returns a 503 if it fails to lookup the data of the IP address.
225
+
226
+ HowAboutNo also inherits the limitations of ip-api.com, including potential inaccuracies in the data and rate limits on requests. Enabling caching can help mitigate the impact of rate limits, but it's important to be aware of these limitations when using HowAboutNo.
227
+
228
+ > [!WARNING]
229
+ HowAboutNo relies on ip-api.com for its blocking functionality, and ip-api.com has its own terms of service which must be followed. Make sure to review ip-api.com's terms of service before using HowAboutNo, especially if you plan to use it for commercial purposes.
230
+
231
+ ### Caching
232
+ HowAboutNo uses an LRU cache to store IP lookup results from ip-api.com.
233
+
234
+ To disable caching, set `size` to `0`.
235
+ To disable cache invalidation, set `invalidate_success_after` or `invalidate_error_after` to `0` according to your needs.
236
+
237
+ - **When caching and invalidation are enabled:**
238
+ - On the first request from an IP address, its data is looked up and cached (with eviction if the cache is full).
239
+ - On subsequent requests from the same IP:
240
+ - If the cache entry is successful (status code 200) and time elapsed since caching exceeds `invalidate_success_after`, or if the cache entry is an error (non-200 status code) and time elapsed since caching exceeds `invalidate_error_after`, the IP is looked up again and the cache entry is refreshed.
241
+ - Otherwise, the cached data is returned.
242
+
243
+ - **When caching is enabled but invalidation is disabled:**
244
+ - On the first request from an IP address, its data is looked up and cached, evicting older entries if necessary.
245
+ - On subsequent requests from the same IP, the cached data is returned indefinitely until it is evicted due to cache size limits.
246
+ - This applies individually to successful and error entries based on the `invalidate_success_after` and `invalidate_error_after` settings.
247
+
248
+ - **When caching is disabled:**
249
+ - The IP address is looked up on every request.
250
+
251
+ ## FAQ
252
+ ### General
253
+ - **Q: Will this middleware work with any ASGI framework?**
254
+ - A: Yes, HowAboutNo is built to be compatible with any ASGI framework. You can wrap any ASGI app with HowAboutNo to add the blocking functionality.
255
+ - **Q: Why can't I use Cloudflare or a similar service instead of this middleware?**
256
+ - A: To avoid corporate dependencies and have full control over the blocking logic and responses. HowAboutNo allows you to define custom blocking rules and responses without relying on third-party services, which can be important for privacy, security, and customization reasons.
257
+ - **Q: Is HowAboutNo free to use?**
258
+ - A: Yes, HowAboutNo is open source and free to use under the MIT License.
259
+ - **Q: Why ASGI middleware and not WSGI middleware?**
260
+ - A: To be honest, I don't have a specific reason for choosing ASGI over WSGI. I chose ASGI simply because it's the modern standard for Python web applications.
261
+ - **Q: Can I use HowAboutNo with a WSGI app?**
262
+ - A: Not directly, since HowAboutNo is designed as ASGI middleware. However, you can use an ASGI-to-WSGI adapter like `asgiref.wsgi.WsgiToAsgi` to wrap your WSGI app and then apply HowAboutNo as ASGI middleware to the wrapped app.
263
+ - **Q: Can I use this for commercial purposes?**
264
+ - A: Yes, but also no, you can use the source code for commercial purposes. However, the IP lookup functionality relies on [ip-api.com](http://ip-api.com/), which has its own terms of service which does not allow commercial use without a paid plan.
265
+ - **Q: Why such a casual name for a middleware?**
266
+ - A: Because I said so!
267
+ - **Q: Will I get a girlfriend by using this middleware?**
268
+ - A: Uh, maybe? Who knows!
269
+
270
+ ### Architecture
271
+ - **Q: Will my app be affected by [ip-api.com](https://ip-api.com/) rate limits?**
272
+ - A: Yes, but not in the way you might think. Caching is implemented to minimize the number of requests made to ip-api.com, which helps avoid hitting rate limits. However, if you do hit a rate limit, HowAboutNo will return a 503 Service Unavailable response for requests that require an IP lookup until the rate limit resets. This means that your app will not be able to perform IP lookups during this time, but it will still be able to serve requests that do not require IP lookups (e.g., requests from IPs that are already in the cache).
273
+ - **Q: What happens if ip-api.com is unreachable or returns an error?**
274
+ - A: If ip-api.com returns an error or is unreachable, HowAboutNo will treat the lookup as unsuccessful and will return a 503 Service Unavailable response.
275
+ - **Q: If multiple block conditions match for a request, which response is returned?**
276
+ - A: Only the response for the first matching block condition is returned. The block conditions are checked in the following order: IP, continent, country, ASN, rDNS hostname, hosting status, and proxy status. So if a request matches both `block_country` and `block_asn`, the response defined for `block_country` will be returned.
277
+ - **Q: Can I exclude certain IPs or paths from being blocked?**
278
+ - A: Yes, you can use the `exception_ip` and `exception_path` settings in the configuration file to specify IP addresses and URL paths that should be excluded from blocking rules. If a request matches a block condition but the IP is in `exception_ip` or the path is in `exception_path`, the request will not be blocked based on that condition.
279
+ - **Q: What happens if an IP address matches a block condition but is also in the exception list?**
280
+ - A: If an IP address matches a block condition but is also listed in `exception_ip`, the blocking rules will be bypassed for that IP address, and the request will not be blocked based on that condition. The same applies to URL paths listed in `exception_path`. This allows you to have granular control over which IPs and paths are subject to blocking rules.
281
+ - **Q: What happens if an IP address is present in both `block_ip` and `exception_ip`?**
282
+ - A: If an IP address is present in both `block_ip` and `exception_ip`, the blocking rules will be bypassed for that IP address, and the request will not be blocked based on the `block_ip` condition. The presence of the IP address in `exception_ip` takes precedence over its presence in `block_ip`.
283
+ - **Q: If caching is enabled and an IP address is blocked based on a cached entry, but the actual data for that IP has changed since it was cached, will the middleware still block the request?**
284
+ - A: Yes, if the cache entry for that IP address indicates that it should be blocked, the middleware will block the request based on the cached data. The cache invalidation settings determine how long entries remain in the cache before they are refreshed, but while they are in the cache, their data is used for blocking decisions.
285
+ - **Q: What do `invalidate_success_after` and `invalidate_error_after` do?**
286
+ - A: `invalidate_success_after` specifies the time in seconds after which a successful cache entry (status code 200) should be invalidated and refreshed on the next request. `invalidate_error_after` specifies the time in seconds after which an unsuccessful cache entry (non-200 status code) should be invalidated and refreshed on the next request. Setting either of these to `0` disables invalidation for that type of entry, meaning entries will remain in the cache indefinitely until evicted due to cache size limits.
287
+
288
+ ### Troubleshooting
289
+ - **Q: I'm seeing a lot of 503 Service Unavailable responses. What could be causing this?**
290
+ - A: This could be caused by several factors:
291
+ - You might be hitting the rate limits of ip-api.com, especially if caching is disabled or if you have a high volume of requests from unique IP addresses. In this case, you would need to wait until the rate limit resets.
292
+ - ip-api.com might be experiencing downtime or issues, which would cause lookups to fail and result in 503 responses. You can check the status of ip-api.com to see if there are any reported issues.
293
+ - There could be a network issue preventing your server from reaching ip-api.com, which would also lead to failed lookups and 503 responses. You can check your server's network connectivity to ensure it can reach ip-api.com.
294
+
295
+ - **Q: My configuration changes are not taking effect. What should I do?**
296
+ - A: The configuration file is read when the HowAboutNo middleware is initialized. If you make changes to the configuration file after the middleware has been initialized, those changes will not take effect until the middleware is reloaded. To apply configuration changes, you will need to restart your ASGI application so that the HowAboutNo middleware can read the updated configuration file.
297
+
298
+ Anything else? Please let me know by opening an issue!
299
+
300
+ ## LICENSE
301
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
302
+
303
+ ## Acknowledgements
304
+ - [ip-api.com](https://ip-api.com/) for providing the IP data.
305
+ - [bitwire-it](https://github.com/bitwire-it) for the [IP blocklist](https://github.com/bitwire-it/ipblocklist/) data.
306
+
307
+ If you found this project useful, please consider giving it a star!