From 47683c4704572bbe0efb3b989b35a3912b65ac83 Mon Sep 17 00:00:00 2001 From: Andrew Clayton Date: Mon, 15 May 2023 22:48:31 +0100 Subject: Python: Fix ASGI applications accessed over IPv6. There are a couple of reports on GitHub about issues accessing Python ASGI based applications over IPv6. A request over IPv6 would result in an error like 2023/05/13 17:49:12 [alert] 47202#47202 [unit] #10: Python failed to create 'client' pair 2023/05/13 17:49:12 [alert] 47202#47202 [unit] Python failed to call 'loop.call_soon' ValueError: invalid literal for int() with base 10: 'db8:1:1:1ee7:dead:beef:cafe' The above error was the direct cause of the following exception: Traceback (most recent call last): File "/usr/lib64/python3.11/asyncio/base_events.py", line 765, in call_soon handle = self._call_soon(callback, args, context) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib64/python3.11/asyncio/base_events.py", line 781, in _call_soon handle = events.Handle(callback, args, self, context) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SystemError: returned a result with an exception set This issue occurred in the nxt_py_asgi_create_ip_address() function where it tries to create an IP address / port number pair. It does this by looking for the first ':' in the address and taking everything after it as the port number. Like in the above error message, if we tried to access the server @ 2001:db8:1:1:1ee7:dead:beef:cafe, then we'd end up with the port number as 'db8:1:1:1ee7:dead:beef:cafe'. There are two issues with this 1) The IP address and port number are already flowed through separately. 2) Even if (1) wasn't true, it would still be broken for IPv6 as we'd expect to a get an address literal like [2001:db8:1:1:1ee7:dead:beef:cafe]:8080, however there was no code to handle the []'s. The fix is to simply not try looking for a port number. We pass a port number into this function to use in the case where we don't find a port number, we never will... A further cleanup would be to flow through the server port number when creating the 'server pair' PyTuple, rather than just using the hard coded 80. Closes: Closes: Reviewed-by: Alejandro Colomar Signed-off-by: Andrew Clayton --- src/python/nxt_python_asgi.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'src/python/nxt_python_asgi.c') diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c index adf03e2b..9f6cde3b 100644 --- a/src/python/nxt_python_asgi.c +++ b/src/python/nxt_python_asgi.c @@ -828,7 +828,7 @@ nxt_py_asgi_create_address(nxt_unit_sptr_t *sptr, uint8_t len, uint16_t port) static PyObject * nxt_py_asgi_create_ip_address(nxt_unit_sptr_t *sptr, uint8_t len, uint16_t port) { - char *p, *s; + char *p; PyObject *pair, *v; pair = PyTuple_New(2); @@ -837,9 +837,8 @@ nxt_py_asgi_create_ip_address(nxt_unit_sptr_t *sptr, uint8_t len, uint16_t port) } p = nxt_unit_sptr_get(sptr); - s = memchr(p, ':', len); - v = PyString_FromStringAndSize(p, s == NULL ? len : s - p); + v = PyString_FromStringAndSize(p, len); if (nxt_slow_path(v == NULL)) { Py_DECREF(pair); @@ -848,14 +847,7 @@ nxt_py_asgi_create_ip_address(nxt_unit_sptr_t *sptr, uint8_t len, uint16_t port) PyTuple_SET_ITEM(pair, 0, v); - if (s != NULL) { - p += len; - v = PyLong_FromString(s + 1, &p, 10); - - } else { - v = PyLong_FromLong(port); - } - + v = PyLong_FromLong(port); if (nxt_slow_path(v == NULL)) { Py_DECREF(pair); -- cgit From 93ed66958e11b40cc06dcb32fc8e623967af6347 Mon Sep 17 00:00:00 2001 From: synodriver Date: Sat, 27 May 2023 22:18:46 +0800 Subject: Python: Add ASGI lifespan state support. Lifespan state is a special dict in asgi lifespan scope, which allow applications to persist data from the lifespan cycle to request/response handling. The scope["state"] namespace provides a place to store these sorts of things. The server will ensure that a shallow copy of the namespace is passed into each subsequent request/response call into the application. Some frameworks are already taking advantage of this feature, for example, starlette, and without this feature they wouldn't work properly. Signed-off-by: synodriver Reviewed-by: Andrew Clayton [ Minor code tweaks to avoid lines > 80 chars, static a function and re-work the PyMemberDef structure initialisation for Python <3.7 and -Wwrite-strings compatibility - Andrew ] Tested-by: Tested-by: Closes: Signed-off-by: Andrew Clayton --- src/python/nxt_python_asgi.c | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) (limited to 'src/python/nxt_python_asgi.c') diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c index 9f6cde3b..5882491b 100644 --- a/src/python/nxt_python_asgi.c +++ b/src/python/nxt_python_asgi.c @@ -450,6 +450,7 @@ static void nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) { PyObject *scope, *res, *task, *receive, *send, *done, *asgi; + PyObject *state, *newstate, *lifespan; PyObject *stage2; nxt_python_target_t *target; nxt_py_asgi_ctx_data_t *ctx_data; @@ -493,15 +494,41 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) } req->data = asgi; + ctx_data = req->ctx->data; target = &nxt_py_targets->target[req->request->app_target]; + lifespan = ctx_data->target_lifespans[req->request->app_target]; + state = PyObject_GetAttr(lifespan, nxt_py_state_str); + if (nxt_slow_path(state == NULL)) { + nxt_unit_req_alert(req, "Python failed to get 'state' attribute"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_done; + } + + newstate = PyDict_Copy(state); + if (nxt_slow_path(newstate == NULL)) { + nxt_unit_req_alert(req, "Python failed to call state.copy()"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + Py_DECREF(state); + goto release_done; + } + Py_DECREF(state); scope = nxt_py_asgi_create_http_scope(req, target); if (nxt_slow_path(scope == NULL)) { nxt_unit_request_done(req, NXT_UNIT_ERROR); - + Py_DECREF(newstate); goto release_done; } + if (nxt_slow_path(PyDict_SetItem(scope, nxt_py_state_str, newstate) + == -1)) + { + Py_DECREF(newstate); + goto release_scope; + } + Py_DECREF(newstate); + if (!target->asgi_legacy) { nxt_unit_req_debug(req, "Python call ASGI 3.0 application"); @@ -555,7 +582,6 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) goto release_scope; } - ctx_data = req->ctx->data; task = PyObject_CallFunctionObjArgs(ctx_data->loop_create_task, res, NULL); if (nxt_slow_path(task == NULL)) { -- cgit From b84f6ecad42f4217b6eafb0ceb1e66a75b34e948 Mon Sep 17 00:00:00 2001 From: synodriver Date: Sat, 27 May 2023 23:18:41 +0800 Subject: Python: Fix error checks in nxt_py_asgi_request_handler(). Signed-off-by: synodriver Reviewed-by: Andrew Clayton [ Re-word commit subject - Andrew ] Fixes: c4c2f90c5b53 ("Python: ASGI server introduced.") Closes: Signed-off-by: Andrew Clayton --- src/python/nxt_python_asgi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/python/nxt_python_asgi.c') diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c index 5882491b..8f300b53 100644 --- a/src/python/nxt_python_asgi.c +++ b/src/python/nxt_python_asgi.c @@ -478,7 +478,7 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) } send = PyObject_GetAttrString(asgi, "send"); - if (nxt_slow_path(receive == NULL)) { + if (nxt_slow_path(send == NULL)) { nxt_unit_req_alert(req, "Python failed to get 'send' method"); nxt_unit_request_done(req, NXT_UNIT_ERROR); @@ -486,7 +486,7 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) } done = PyObject_GetAttrString(asgi, "_done"); - if (nxt_slow_path(receive == NULL)) { + if (nxt_slow_path(done == NULL)) { nxt_unit_req_alert(req, "Python failed to get '_done' method"); nxt_unit_request_done(req, NXT_UNIT_ERROR); -- cgit