#define PY_SSIZE_T_CLEAN #include #include #include #include #include "lib/llhttp.h" #define STRING(x) #x #define XSTRING(x) STRING(x) #define LLHTTP_VERSION XSTRING(LLHTTP_VERSION_MAJOR) "." XSTRING(LLHTTP_VERSION_MINOR) "." XSTRING(LLHTTP_VERSION_PATCH) /* What this module is called, in one place. It is the name the types and the exception * classes carry, and the name they are looked back up under at runtime. It used to be * `pyllhttp` — the archived package this binding was forked from — which is not a * dependency and is not installed anywhere this is shipped. */ #define MODULE_NAME "firegex._llhttp" typedef struct{ PyObject_HEAD llhttp_t llhttp; } llhttp_obj_t; typedef struct{ llhttp_errno_t code; const char *name; } error_info; static error_info errors[] = { #define HTTP_ERRNO_GEN(CODE, NAME, _) {CODE, #NAME}, HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) #undef HTTP_ERRNO_GEN }; /* * `INVALID_METHOD` -> `InvalidMethodError`: the name this module publishes the * exception for that code under. * * Written into the caller's buffer every time, never cached on the table above. That * table is one array shared by every interpreter in the process, and this module * declares per-interpreter GIL support — a name stored there is a name two interpreters * can be writing at the same moment. It used to be `malloc`ed on first use, guarded by * a NULL check two threads can pass together, and never freed. */ static void error_class_name(const char *snake, char *out, size_t out_len) { size_t written = 0; bool upper = true; for (const char *c = snake; *c && written + 1 < out_len; ++c) { if (isalpha((unsigned char)*c)) out[written++] = (char)(upper ? toupper((unsigned char)*c) : tolower((unsigned char)*c)); else if (isdigit((unsigned char)*c)) out[written++] = *c; upper = !isalpha((unsigned char)*c); } for (const char *suffix = "Error"; *suffix && written + 1 < out_len; ++suffix) out[written++] = *suffix; out[written] = '\0'; } static const char* methods[] = { #define HTTP_METHOD_GEN(NUMBER, NAME, STRING) #STRING, HTTP_METHOD_MAP(HTTP_METHOD_GEN) #undef HTTP_METHOD_GEN }; /* New public callback helper that uses method names as C strings */ static int parser_callback(const char *name, llhttp_t *llhttp) { PyObject *result = PyObject_CallMethod(llhttp->data, name, NULL); if (result) Py_DECREF(result); if (PyErr_Occurred()) return HPE_USER; if (HPE_PAUSED == llhttp_get_errno(llhttp)) { llhttp_resume(llhttp); return HPE_PAUSED; } if (HPE_PAUSED_UPGRADE == llhttp_get_errno(llhttp)) { llhttp_resume_after_upgrade(llhttp); return HPE_PAUSED_UPGRADE; } return HPE_OK; } static int parser_data_callback(const char *name, llhttp_t *llhttp, const char *data, size_t length) { PyObject *payload = PyMemoryView_FromMemory((char*)data, length, PyBUF_READ); if (!payload) return HPE_USER; PyObject *result = PyObject_CallMethod(llhttp->data, name, "O", payload); Py_DECREF(payload); if (result) Py_DECREF(result); if (PyErr_Occurred()) return HPE_USER; if (HPE_PAUSED == llhttp_get_errno(llhttp)) { llhttp_resume(llhttp); return HPE_PAUSED; } if (HPE_PAUSED_UPGRADE == llhttp_get_errno(llhttp)) { llhttp_resume_after_upgrade(llhttp); return HPE_PAUSED_UPGRADE; } return HPE_OK; } /* Macros now simply pass the name as a string literal */ #define PARSER_CALLBACK(type) \ static int parser_ ## type (llhttp_t *llhttp) { \ return parser_callback(#type, llhttp); \ } #define PARSER_DATA_CALLBACK(type) \ static int parser_ ## type (llhttp_t *llhttp, const char *data, size_t length) { \ return parser_data_callback(#type, llhttp, data, length); \ } PARSER_CALLBACK(on_message_begin) PARSER_DATA_CALLBACK(on_url) PARSER_CALLBACK(on_url_complete) PARSER_DATA_CALLBACK(on_status) PARSER_CALLBACK(on_status_complete) PARSER_DATA_CALLBACK(on_header_field) PARSER_CALLBACK(on_header_field_complete) PARSER_DATA_CALLBACK(on_header_value) PARSER_CALLBACK(on_header_value_complete) PARSER_CALLBACK(on_headers_complete) PARSER_DATA_CALLBACK(on_body) PARSER_CALLBACK(on_message_complete) PARSER_CALLBACK(on_chunk_header) PARSER_CALLBACK(on_chunk_complete) llhttp_settings_t parser_settings = { .on_message_begin = parser_on_message_begin, .on_url = parser_on_url, .on_url_complete = parser_on_url_complete, .on_status = parser_on_status, .on_status_complete = parser_on_status_complete, .on_header_field = parser_on_header_field, .on_header_field_complete = parser_on_header_field_complete, .on_header_value = parser_on_header_value, .on_header_value_complete = parser_on_header_value_complete, .on_headers_complete = parser_on_headers_complete, .on_body = parser_on_body, .on_message_complete = parser_on_message_complete, .on_chunk_header = parser_on_chunk_header, .on_chunk_complete = parser_on_chunk_complete, }; static PyObject *request_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *self = type->tp_alloc(type, 0); if (self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_init(llhttp, HTTP_REQUEST, &parser_settings); llhttp->data = self; } return self; } static PyObject *response_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *self = type->tp_alloc(type, 0); if (self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_init(llhttp, HTTP_RESPONSE, &parser_settings); llhttp->data = self; } return self; } /* * Turn an llhttp error code into the exception this module publishes for it. * * The classes are looked up in the module itself — `sys.modules` belongs to the * interpreter asking, so each one finds the classes it built, which is the whole reason * they are created per module instance rather than once per process. * * This used to import `pyllhttp`, a package that is not a dependency and is installed * nowhere the wheel is: the import failed, its traceback was printed to stderr (the * service log, on a filter), and the function returned **without setting an exception**, * which CPython reports to the caller as `SystemError: ... returned NULL without setting * an exception`. A malformed request — the traffic this exists to inspect — said that * instead of naming the parse error. So every path out of here raises something. */ static void set_related_exception(llhttp_errno_t eno, llhttp_t *llhttp) { const char *reason = llhttp_get_error_reason(llhttp); if (!reason) reason = "HTTP parse error"; for (size_t i = 0; i < sizeof(errors)/sizeof(errors[0]); ++i) { if (errors[i].code != eno) continue; char class_name[128]; error_class_name(errors[i].name, class_name, sizeof(class_name)); PyObject *module = PyImport_ImportModule(MODULE_NAME); if (module) { PyObject *exception = PyObject_GetAttrString(module, class_name); Py_DECREF(module); if (exception) { PyErr_SetString(exception, reason); Py_DECREF(exception); return; } } /* The class is missing, which should not happen — but the traffic was still * malformed, and saying so badly beats saying nothing. */ PyErr_Clear(); PyErr_Format(PyExc_RuntimeError, "%s: %s", class_name, reason); return; } PyErr_Format(PyExc_RuntimeError, "unknown HTTP parser error %d: %s", (int)eno, reason); } static PyObject *parser_execute(PyObject *self, PyObject *payload) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; Py_buffer buffer; if (PyObject_GetBuffer(payload, &buffer, PyBUF_SIMPLE)){ return NULL; } if (!PyBuffer_IsContiguous(&buffer, 'C')) { PyErr_SetString(PyExc_TypeError, "buffer is not contiguous"); PyBuffer_Release(&buffer); return NULL; } llhttp_errno_t error = llhttp_execute(llhttp, buffer.buf, buffer.len); PyBuffer_Release(&buffer); if (PyErr_Occurred()) return NULL; switch (error) { case HPE_OK: return Py_BuildValue("(nn)", error, buffer.len); case HPE_PAUSED: case HPE_PAUSED_UPGRADE: case HPE_PAUSED_H2_UPGRADE: return Py_BuildValue("(nn)", error, llhttp->error_pos - (const char*)buffer.buf); default: set_related_exception(error, llhttp); return NULL; } } static PyObject *parser_pause(PyObject *self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_pause(llhttp); Py_RETURN_NONE; } static PyObject *parser_unpause(PyObject *self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_resume(llhttp); Py_RETURN_NONE; } static PyObject *parser_upgrade(PyObject *self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_resume_after_upgrade(llhttp); Py_RETURN_NONE; } static PyObject *parser_finish(PyObject *self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_errno_t error = llhttp_finish(llhttp); if (HPE_OK == error) Py_RETURN_NONE; set_related_exception(error, llhttp); return NULL; } static PyObject *parser_reset(PyObject *self) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_reset(llhttp); Py_RETURN_NONE; } static PyObject * parser_dummy_noargs(PyObject *self) { Py_RETURN_NONE; } static PyObject * parser_dummy_onearg(PyObject *self, PyObject *arg) { Py_RETURN_NONE; } static PyMethodDef parser_methods[] = { { "execute", (PyCFunction)parser_execute, METH_O }, { "pause", (PyCFunction)parser_pause, METH_NOARGS }, { "unpause", (PyCFunction)parser_unpause, METH_NOARGS }, { "upgrade", (PyCFunction)parser_upgrade, METH_NOARGS }, { "finish", (PyCFunction)parser_finish, METH_NOARGS }, { "reset", (PyCFunction)parser_reset, METH_NOARGS }, { "on_message_begin", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_url", (PyCFunction)parser_dummy_onearg, METH_O }, { "on_url_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_status", (PyCFunction)parser_dummy_onearg, METH_O }, { "on_status_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_header_field", (PyCFunction)parser_dummy_onearg, METH_O }, { "on_header_field_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_header_value", (PyCFunction)parser_dummy_onearg, METH_O }, { "on_header_value_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_headers_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_body", (PyCFunction)parser_dummy_onearg, METH_O }, { "on_message_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_chunk_header", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { "on_chunk_complete", (PyCFunction)parser_dummy_noargs, METH_NOARGS }, { NULL } }; static PyObject *parser_method(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; if (llhttp->type != HTTP_REQUEST) Py_RETURN_NONE; if (!llhttp->http_major && !llhttp->http_minor) Py_RETURN_NONE; return PyUnicode_FromString(methods[llhttp->method]); } static PyObject *parser_major(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; if (!llhttp->http_major && !llhttp->http_minor) Py_RETURN_NONE; return PyLong_FromUnsignedLong(llhttp->http_major); } static PyObject *parser_minor(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; if (!llhttp->http_major && !llhttp->http_minor) Py_RETURN_NONE; return PyLong_FromUnsignedLong(llhttp->http_minor); } /* The numeric status of a response. * * llhttp has had it all along — it is a field on the parser struct, and * `llhttp_get_status_code` reads it — but this binding never exposed it, so the only * thing reachable from Python was the reason phrase the server wrote beside it. A filter * asking "was this a 500?" could not be answered at all. * * `None` for a request, which has no status, and before a response line has been read: * llhttp leaves the field at zero there, and zero is not a status code. */ static PyObject *parser_status_code(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; if (llhttp->type != HTTP_RESPONSE) Py_RETURN_NONE; if (!llhttp->status_code) Py_RETURN_NONE; return PyLong_FromUnsignedLong(llhttp_get_status_code(llhttp)); } static PyObject *parser_content_length(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; if (!(llhttp->flags & F_CONTENT_LENGTH)) Py_RETURN_NONE; return PyLong_FromUnsignedLong(llhttp->content_length); } static bool get_lenient(const llhttp_t *llhttp, llhttp_lenient_flags_t flag) { return llhttp->lenient_flags & flag; } static int set_lenient(llhttp_t *llhttp, llhttp_lenient_flags_t flag, bool value) { if (value) { llhttp->lenient_flags |= flag; } else { llhttp->lenient_flags &= ~flag; } return 0; } #define LENIENT_FLAG(name) \ static PyObject * \ parser_get_lenient_ ## name(PyObject *self, void *closure) \ { return PyBool_FromLong(get_lenient(&((llhttp_obj_t*)self)->llhttp, LENIENT_ ## name)); } \ \ static int \ parser_set_lenient_ ## name(PyObject *self, PyObject *value, void *closure) \ { return set_lenient(&((llhttp_obj_t*)self)->llhttp, LENIENT_ ## name, PyObject_IsTrue(value)); } LENIENT_FLAG(HEADERS); LENIENT_FLAG(CHUNKED_LENGTH); LENIENT_FLAG(KEEP_ALIVE); LENIENT_FLAG(TRANSFER_ENCODING); LENIENT_FLAG(VERSION); static PyObject *parser_get_lenient_headers(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; return PyBool_FromLong(llhttp->lenient_flags & LENIENT_HEADERS); } static int parser_set_lenient_headers(PyObject *self, PyObject *value, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_set_lenient_headers(llhttp, PyObject_IsTrue(value)); return 0; } static PyObject *parser_get_lenient_chunked_length(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; return PyBool_FromLong(llhttp->lenient_flags & LENIENT_CHUNKED_LENGTH); } static int parser_set_lenient_chunked_length(PyObject *self, PyObject *value, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_set_lenient_chunked_length(llhttp, PyObject_IsTrue(value)); return 0; } static PyObject *parser_get_lenient_keep_alive(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; return PyBool_FromLong(llhttp->lenient_flags & LENIENT_KEEP_ALIVE); } static int parser_set_lenient_keep_alive(PyObject *self, PyObject *value, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; llhttp_set_lenient_keep_alive(llhttp, PyObject_IsTrue(value)); return 0; } static PyObject *parser_message_needs_eof(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; return PyBool_FromLong(llhttp_message_needs_eof(llhttp)); } static PyObject *parser_should_keep_alive(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; return PyBool_FromLong(llhttp_should_keep_alive(llhttp)); } static PyObject *parser_is_paused(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; return PyBool_FromLong(HPE_PAUSED == llhttp_get_errno(llhttp)); } static PyObject *parser_is_upgrading(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; switch (llhttp_get_errno(llhttp)) { case HPE_PAUSED_UPGRADE: case HPE_PAUSED_H2_UPGRADE: Py_RETURN_TRUE; break; default: Py_RETURN_FALSE; break; } } static PyObject *parser_is_busted(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; switch (llhttp_get_errno(llhttp)) { case HPE_OK: case HPE_PAUSED: case HPE_PAUSED_UPGRADE: Py_RETURN_FALSE; default: Py_RETURN_TRUE; } } static PyObject *parser_error(PyObject *self, void *closure) { llhttp_t *llhttp = &((llhttp_obj_t*)self)->llhttp; if (HPE_OK == llhttp_get_errno(llhttp)) Py_RETURN_NONE; return PyUnicode_FromString(llhttp_get_error_reason(llhttp)); } static PyGetSetDef parser_getset[] = { { "method", parser_method }, { "status_code", parser_status_code }, { "major", parser_major }, { "minor", parser_minor }, { "content_length", parser_content_length }, { "lenient_headers", parser_get_lenient_HEADERS, parser_set_lenient_HEADERS }, { "lenient_chunked_length", parser_get_lenient_CHUNKED_LENGTH, parser_set_lenient_CHUNKED_LENGTH }, { "lenient_keep_alive", parser_get_lenient_KEEP_ALIVE, parser_set_lenient_KEEP_ALIVE }, { "lenient_transfer_encoding", parser_get_lenient_TRANSFER_ENCODING, parser_set_lenient_TRANSFER_ENCODING }, { "lenient_version", parser_get_lenient_VERSION, parser_set_lenient_VERSION }, { "message_needs_eof", parser_message_needs_eof }, { "should_keep_alive", parser_should_keep_alive }, { "is_paused", parser_is_paused }, { "is_upgrading", parser_is_upgrading }, { "is_busted", parser_is_busted }, { "error", parser_error }, { NULL } }; static void parser_dealloc(PyObject *self) { llhttp_obj_t *llhttp = (llhttp_obj_t*)self; Py_TYPE(self)->tp_free(self); } static PyType_Slot request_slots[] = { {Py_tp_doc, "llhttp request parser"}, {Py_tp_new, request_new}, {Py_tp_dealloc, parser_dealloc}, {Py_tp_methods, parser_methods}, {Py_tp_getset, parser_getset}, {0, 0}, }; static PyType_Spec request_spec = { MODULE_NAME ".Request", sizeof(llhttp_obj_t), 0, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, request_slots, }; static PyType_Slot response_slots[] = { {Py_tp_doc, "llhttp response parser"}, {Py_tp_new, response_new}, {Py_tp_dealloc, parser_dealloc}, {Py_tp_methods, parser_methods}, {Py_tp_getset, parser_getset}, {0, NULL}, }; static PyType_Spec response_spec = { MODULE_NAME ".Response", sizeof(llhttp_obj_t), 0, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, response_slots, }; /* One class per error code, per module instance: two subinterpreters must never share * an exception object, which is what `Py_MOD_PER_INTERPRETER_GIL_SUPPORTED` promises. */ static int add_error_classes(PyObject *m) { PyObject *base_error = PyErr_NewException(MODULE_NAME ".Error", NULL, NULL); if (!base_error) return -1; if (PyModule_AddObjectRef(m, "Error", base_error) < 0) { Py_DECREF(base_error); return -1; } for (size_t i = 0; i < sizeof(errors)/sizeof(errors[0]); ++i) { char class_name[128]; char qualified_name[128 + sizeof(MODULE_NAME) + 1]; error_class_name(errors[i].name, class_name, sizeof(class_name)); snprintf(qualified_name, sizeof(qualified_name), MODULE_NAME ".%s", class_name); PyObject *exception = PyErr_NewException(qualified_name, base_error, NULL); if (!exception) { Py_DECREF(base_error); return -1; } int added = PyModule_AddObjectRef(m, class_name, exception); Py_DECREF(exception); if (added < 0) { Py_DECREF(base_error); return -1; } } Py_DECREF(base_error); return 0; } static int add_type(PyObject *m, PyType_Spec *spec) { PyObject *type = PyType_FromSpec(spec); if (!type) return -1; /* The spec carries the dotted name; the module wants the last component of it. */ int added = PyModule_AddObjectRef(m, spec->name + strlen(MODULE_NAME "."), type); Py_DECREF(type); return added; } static int init_llhttp_module(PyObject *m) { if (PyModule_AddStringConstant(m, "version", LLHTTP_VERSION) < 0) goto fail; for (size_t i = 0; i < sizeof(errors)/sizeof(errors[0]); ++i) { if (PyModule_AddIntConstant(m, errors[i].name, errors[i].code) < 0) goto fail; } if (add_error_classes(m) < 0) goto fail; if (add_type(m, &request_spec) < 0) goto fail; if (add_type(m, &response_spec) < 0) goto fail; return 0; fail: /* The module belongs to the import machinery, which destroys it when a `Py_mod_exec` * slot fails; releasing it here as well handed back a reference that was never ours. * Whatever raised first is the useful message, so nothing is overwritten either. */ if (!PyErr_Occurred()) PyErr_SetString(PyExc_RuntimeError, "Failed to initialize the llhttp module"); return -1; } static PyModuleDef_Slot llhttp_slots[] = { {Py_mod_exec, init_llhttp_module}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {0, NULL} }; static struct PyModuleDef llhttp_module = { PyModuleDef_HEAD_INIT, .m_name = "firegex._llhttp", .m_doc = "llhttp wrapper", .m_slots = llhttp_slots, .m_size = 0, }; PyMODINIT_FUNC PyInit__llhttp(void) { return PyModuleDef_Init(&llhttp_module); }