-
Notifications
You must be signed in to change notification settings - Fork 14
add uvicorn context-creation support when run with gUnicorn #576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 45 commits
a2d632e
d708e59
44b28b0
dbdd8f7
fc3c5fd
3e17be0
fa05e8d
d86a170
706bde2
bc6b241
c4d1a53
b02ecca
840b0c5
8448c56
a9818dc
a33f58d
4959447
5e3587a
7f97945
8b80276
0e1097a
f6ea71f
924d1fc
a0ad361
94dd5f6
47038d8
3d2899a
cb5972d
e306e78
c8292ec
0b0d2de
a2a42f1
e5362f3
5acd6dc
9f2323d
0b51773
96d5f5c
8e7db0d
4226b9b
2b1714a
f8a3ddc
bbd61ae
1a40cfa
596709f
a5f1bf6
c0bdc78
4836a3c
af0b50c
cde98e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import inspect | ||
| from aikido_zen.context import Context | ||
| from aikido_zen.helpers.get_argument import get_argument | ||
| from aikido_zen.sinks import before_async, patch_function | ||
| from aikido_zen.sources.functions.request_handler import request_handler, post_response | ||
| from aikido_zen.thread.thread_cache import get_cache | ||
|
|
||
|
|
||
| class InternalASGIMiddleware: | ||
| def __init__(self, app, source: str): | ||
| self.client_app = app | ||
| self.source = source | ||
|
|
||
| async def __call__(self, scope, receive, send): | ||
| if not scope or scope.get("type") != "http": | ||
| # Zen checks requests coming into HTTP(S) server, ignore other requests (like ws) | ||
| return await self.continue_app(scope, receive, send) | ||
|
|
||
| context = Context(req=scope, source=self.source) | ||
|
|
||
| process_cache = get_cache() | ||
| if process_cache and process_cache.is_bypassed_ip(context.remote_address): | ||
| # IP address is bypassed, for simplicity we do not set a context, | ||
| # and we do not do any further handling of the request. | ||
| return await self.continue_app(scope, receive, send) | ||
|
|
||
| context.set_as_current_context() | ||
| if process_cache: | ||
| # Since this SHOULD be the highest level of the apps we wrap, this is the safest place | ||
| # to increment total hits. | ||
| process_cache.stats.increment_total_hits() | ||
|
|
||
| intercept_response = request_handler(stage="pre_response") | ||
| if intercept_response: | ||
| # The request has already been blocked (e.g. IP is on blocklist) | ||
| return await send_status_code_and_text(send, intercept_response) | ||
|
|
||
| return await self.run_with_intercepts(scope, receive, send) | ||
|
|
||
| async def run_with_intercepts(self, scope, receive, send): | ||
| # We use a skeleton class so we can use patch_function (and the logic already defined in @before_async) | ||
| class InterceptorSkeleton: | ||
| @staticmethod | ||
| async def send(*args, **kwargs): | ||
| return await send(*args, **kwargs) | ||
|
|
||
| patch_function(InterceptorSkeleton, "send", send_interceptor) | ||
|
|
||
| return await self.continue_app(scope, receive, InterceptorSkeleton.send) | ||
|
|
||
| async def continue_app(self, scope, receive, send): | ||
| client_app_parameters = len(inspect.signature(self.client_app).parameters) | ||
| if client_app_parameters == 2: | ||
| # This is possible if the app is still using ASGI v2.0 | ||
| # See https://asgi.readthedocs.io/en/latest/specs/main.html#legacy-applications | ||
| # client_app = coroutine application_instance(receive, send) | ||
| await self.client_app(receive, send) | ||
| else: | ||
| # client_app = coroutine application(scope, receive, send) | ||
| await self.client_app(scope, receive, send) | ||
|
|
||
|
|
||
| async def send_status_code_and_text(send, pre_response): | ||
| await send( | ||
| { | ||
| "type": "http.response.start", | ||
| "status": pre_response[1], | ||
| "headers": [(b"content-type", b"text/plain")], | ||
| } | ||
| ) | ||
| await send( | ||
| { | ||
| "type": "http.response.body", | ||
| "body": pre_response[0].encode("utf-8"), | ||
| "more_body": False, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| @before_async | ||
| async def send_interceptor(func, instance, args, kwargs): | ||
| # There is no name for the send() comment in the standard, it really depends (quart uses message) | ||
| event = get_argument(args, kwargs, 0, name="message") | ||
|
|
||
| # https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event | ||
| if not event or "http.response.start" not in event.get("type", ""): | ||
| # If the event is not of type http.response.start it won't contain the status code. | ||
| # And this event is required before sending over a body (so even 200 status codes are intercepted). | ||
| return | ||
|
|
||
| if "status" in event: | ||
| # Handle post response logic (attack waves, route reporting, ...) | ||
| post_response(status_code=int(event.get("status"))) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from aikido_zen.helpers.get_argument import get_argument | ||
| from aikido_zen.sinks import before, on_import, patch_function | ||
| from aikido_zen.helpers.logging import logger | ||
| from aikido_zen.sources.functions.asgi_middleware import InternalASGIMiddleware | ||
|
|
||
|
|
||
| @before | ||
| def _init(func, instance, args, kwargs): | ||
| config = get_argument(args, kwargs, 0, "config") | ||
| if not config.app: | ||
| return | ||
| logger.debug("Server detected as: Uvicorn, Wrapping ASGI app with Zen.") | ||
| config.app = InternalASGIMiddleware(config.app, "uvicorn") | ||
|
|
||
|
|
||
| @on_import("uvicorn.server", "uvicorn") | ||
| def patch(m): | ||
| """ | ||
| We patch uvicorn.server, so that we can modify the app to go through our ASGI middleware first | ||
| """ | ||
| patch_function(m, "Server.__init__", _init) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Django ASGI | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we document the caveats? of the limited support? e.g. https://github.com/AikidoSec/firewall-node/blob/main/docs/next.md#caveats
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are no caveats, the limited support is about with which frameworks it works? not sure how I am supposed to clarify that further, is already in bold under ## Support (as in limited support) |
||
|
|
||
| ## Support | ||
| Currently Django ASGI is **only supported with gUnicorn** and a uvicorn worker. | ||
|
|
||
|
|
||
| ## Installation for gUnicorn with a Uvicorn Worker | ||
| 1. Install `aikido_zen` package with pip : | ||
| ```sh | ||
| pip install aikido_zen | ||
| ``` | ||
|
|
||
| 2. Use the following template for your `gunicorn_config.py` file : | ||
| ```python | ||
| import aikido_zen.decorators.gunicorn as aik | ||
|
|
||
| @aik.post_fork | ||
| def post_fork(server, worker): | ||
| # If you already have a config file, replace pass with your own code. | ||
| pass | ||
| ``` | ||
| And make sure to include this config when starting gunicorn by adding the `-c gunicorn_config.py` flag. | ||
| ```sh | ||
| gunicorn -c gunicorn_config.py --workers ... | ||
| ``` | ||
|
|
||
| 3. Setting your environment variables : | ||
| Make sure to set your token in order to communicate with Aikido's servers | ||
| ```env | ||
| AIKIDO_TOKEN="AIK_RUNTIME_YOUR_TOKEN_HERE" | ||
| ``` | ||
|
|
||
| ## Blocking mode | ||
|
|
||
| By default, the firewall will run in non-blocking mode. When it detects an attack, the attack will be reported to Aikido and continue executing the call. | ||
|
|
||
| You can enable blocking mode by setting the environment variable `AIKIDO_BLOCK` to `true`: | ||
|
|
||
| ```sh | ||
| AIKIDO_BLOCK=true | ||
| ``` | ||
|
|
||
| It's recommended to enable this on your staging environment for a considerable amount of time before enabling it on your production environment (e.g. one week). | ||
|
|
||
| ## Debug mode | ||
|
|
||
| If you need to debug the firewall, you can run your code with the environment variable `AIKIDO_DEBUG` set to `true`: | ||
|
|
||
| ```sh | ||
| AIKIDO_DEBUG=true | ||
| ``` | ||
|
|
||
| This will output debug information to the console (e.g. no token was found, unsupported packages, extra information, ...). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| include ../common.mk | ||
|
|
||
| PORT = 8114 | ||
| PORT_DISABLED = 8115 | ||
|
|
||
| .PHONY: run | ||
| run: install | ||
| @echo "Running sample app django-asgi-uvicorn with Zen on port $(PORT)" | ||
| poetry run python manage.py migrate || true | ||
|
|
||
| OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES \ | ||
| $(AIKIDO_ENV_COMMON) \ | ||
| poetry run gunicorn -c gunicorn_config.py --workers 4 --log-level debug --access-logfile '-' --error-logfile '-' --bind 0.0.0.0:$(PORT) sample-django-asgi-uvicorn-app.asgi:application | ||
|
|
||
| .PHONY: runZenDisabled | ||
| runZenDisabled: install | ||
| @echo "Running sample app django-asgi-uvicorn without Zen on port $(PORT_DISABLED)" | ||
| poetry run python manage.py migrate || true | ||
| OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES \ | ||
| $(AIKIDO_ENV_DISABLED) \ | ||
| poetry run gunicorn -c gunicorn_config.py --workers 4 --log-level debug --access-logfile '-' --error-logfile '-' --bind 0.0.0.0:$(PORT_DISABLED) sample-django-asgi-uvicorn-app.asgi:application |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Sample Django ASGI App with Gunicorn + Uvicorn | ||
| This is a Django ASGI application running with Gunicorn using the UvicornWorker. | ||
|
|
||
| ## Starting the app | ||
|
|
||
| Run: | ||
| ```bash | ||
| make run # Runs app with zen | ||
| make runZenDisabled # Runs app with zen disabled. | ||
| ``` | ||
|
|
||
| ## URLS | ||
| - Homepage: `http://localhost:8114/app` | ||
| - Create a dog: `http://localhost:8114/app/create/<dog_name>` | ||
| - SQL injection attack example: `Malicious dog", "Injected wrong boss name"); -- ` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import aikido_zen.decorators.gunicorn as aik | ||
|
|
||
| @aik.post_fork | ||
| def post_fork(server, worker): pass | ||
|
|
||
| # Use a UvicornWorker | ||
| worker_class = 'uvicorn.workers.UvicornWorker' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #!/usr/bin/env python | ||
| """Django's command-line utility for administrative tasks.""" | ||
| import os | ||
| import sys | ||
|
|
||
|
|
||
| def main(): | ||
| """Run administrative tasks.""" | ||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample-django-asgi-uvicorn-app.settings') | ||
| try: | ||
| from django.core.management import execute_from_command_line | ||
| except ImportError as exc: | ||
| raise ImportError( | ||
| "Couldn't import Django. Are you sure it's installed and " | ||
| "available on your PYTHONPATH environment variable? Did you " | ||
| "forget to activate a virtual environment?" | ||
| ) from exc | ||
| execute_from_command_line(sys.argv) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for return here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wdym, returning the value? not done for ASGI apps