From 5ba76f65f5c2ea522a2e6629bd845918258a80cf Mon Sep 17 00:00:00 2001 From: Daniel-Constantin Mierla Date: Mon, 8 Apr 2024 21:25:11 +0200 Subject: [PATCH] nghttp2: new module for supporting direct http2 connections - initial import version --- src/modules/nghttp2/Makefile | 44 ++ src/modules/nghttp2/README | 180 +++++ src/modules/nghttp2/doc/Makefile | 4 + src/modules/nghttp2/doc/nghttp2.xml | 36 + src/modules/nghttp2/doc/nghttp2_admin.xml | 180 +++++ src/modules/nghttp2/nghttp2_mod.c | 563 ++++++++++++++++ src/modules/nghttp2/nghttp2_server.c | 760 ++++++++++++++++++++++ src/modules/nghttp2/nghttp2_server.h | 89 +++ 8 files changed, 1856 insertions(+) create mode 100644 src/modules/nghttp2/Makefile create mode 100644 src/modules/nghttp2/README create mode 100644 src/modules/nghttp2/doc/Makefile create mode 100644 src/modules/nghttp2/doc/nghttp2.xml create mode 100644 src/modules/nghttp2/doc/nghttp2_admin.xml create mode 100644 src/modules/nghttp2/nghttp2_mod.c create mode 100644 src/modules/nghttp2/nghttp2_server.c create mode 100644 src/modules/nghttp2/nghttp2_server.h diff --git a/src/modules/nghttp2/Makefile b/src/modules/nghttp2/Makefile new file mode 100644 index 00000000000..d6b2dd84674 --- /dev/null +++ b/src/modules/nghttp2/Makefile @@ -0,0 +1,44 @@ +# +# WARNING: do not run this directly, it should be run by the main Makefile + +include ../../Makefile.defs +auto_gen= +NAME=nghttp2.so + +ifeq ($(CROSS_COMPILE),) + LNGHTTP2_BUILDER=$(shell \ + if pkg-config --exists libnghttp2; then \ + echo 'pkg-config libnghttp2'; \ + else \ + which curl-config; \ + fi) +endif + +ifneq ($(LNGHTTP2_BUILDER),) + LNGHTTP2DEFS = $(shell $(LNGHTTP2_BUILDER) --cflags) + LNGHTTP2LIBS = $(shell $(LNGHTTP2_BUILDER) --libs) +else + LNGHTTP2DEFS = -I$(LOCALBASE)/include -I$(SYSBASE)/include + LNGHTTP2LIBS = -L$(LOCALBASE)/lib -L$(SYSBASE)/lib -lnghttp2 -levent +endif + + +ifeq ($(CROSS_COMPILE),) +LEVENT_BUILDER = $(shell \ + if pkg-config --exists libevent; then \ + echo 'pkg-config libevent'; \ + fi) +endif + +ifeq ($(LEVENT_BUILDER),) + LEVENTDEFS=-I$(LOCALBASE)/include -I$(SYSBASE)/include + LEVENTLIBS=-L$(LOCALBASE)/lib -levent +else + LEVENTDEFS = $(shell $(LEVENT_BUILDER) --cflags) + LEVENTLIBS = $(shell $(LEVENT_BUILDER) --libs) +endif + +DEFS+=$(LNGHTTP2DEFS) $(LEVENTDEFS) +LIBS=$(LNGHTTP2LIBS) $(LEVENTLIBS) + +include ../../Makefile.modules diff --git a/src/modules/nghttp2/README b/src/modules/nghttp2/README new file mode 100644 index 00000000000..21c296ca0a3 --- /dev/null +++ b/src/modules/nghttp2/README @@ -0,0 +1,180 @@ +NGHTTP2 Module + +Daniel-Constantin Mierla + + + +Edited by + +Daniel-Constantin Mierla + + + + Copyright © 2024 asipto.com + __________________________________________________________________ + + Table of Contents + + 1. Admin Guide + + 1. Overview + 2. Dependencies + + 2.1. Kamailio Modules + 2.2. External Libraries or Applications + + 3. Parameters + + 3.1. listen_addr (str) + 3.2. listen_port (str) + 3.3. event_callback (str) + + 4. Functions + + 4.1. nghttp2_reply(code, reason, ctype, body) + + 5. Event Routes + + 5.1. nghttp2:request + + List of Examples + + 1.1. Set listen_addr parameter + 1.2. Set listen_port parameter + 1.3. Set event_callback parameter + 1.4. nghttp2_reply usage + +Chapter 1. Admin Guide + + Table of Contents + + 1. Overview + 2. Dependencies + + 2.1. Kamailio Modules + 2.2. External Libraries or Applications + + 3. Parameters + + 3.1. listen_addr (str) + 3.2. listen_port (str) + 3.3. event_callback (str) + + 4. Functions + + 4.1. nghttp2_reply(code, reason, ctype, body) + + 5. Event Routes + + 5.1. nghttp2:request + +1. Overview + + This module implements an embedded HTTP/2 server using nghttpd2 + library. + +2. Dependencies + + 2.1. Kamailio Modules + 2.2. External Libraries or Applications + +2.1. Kamailio Modules + + The following modules must be loaded before this module: + * none. + +2.2. External Libraries or Applications + + The following libraries or applications must be installed before + running Kamailio with this module loaded: + * libnghttp2 - libnghttpd library (v1.61.0+) + +3. Parameters + + 3.1. listen_addr (str) + 3.2. listen_port (str) + 3.3. event_callback (str) + +3.1. listen_addr (str) + + IPv4 address to listen for HTTP2 connection. If not set, then it + listens on all local addresses (port has to be specified by listen_port + parameter). + + Default value is "" (empty - not set). + + Example 1.1. Set listen_addr parameter +... +modparam("nghttp2", "listen_addr", "127.0.0.1") +... + +3.2. listen_port (str) + + Port or service name to listen for HTTP2 connection. + + Default value is "8282". + + Example 1.2. Set listen_port parameter +... +modparam("nghttp2", "listen_port", "8284") +... + +3.3. event_callback (str) + + The name of the function in the kemi configuration file (embedded + scripting language such as Lua, Python, ...) to be executed instead of + event_route[nghttp2:request] block. + + The function has one string parameter with the value "nghttp2:request". + + Default value is 'empty' (no function is executed for events). + + Example 1.3. Set event_callback parameter +... +modparam("nghttp2", "event_callback", "ksr_nghttp2_event") +... +-- event callback function implemented in Lua +function ksr_nghttp2_event(evname) + KSR.info("===== nghttp2 module triggered event: " .. evname .. "\n"); + return 1; +end +... + +4. Functions + + 4.1. nghttp2_reply(code, reason, ctype, body) + +4.1. nghttp2_reply(code, reason, ctype, body) + + Send back a reply with content-type and body. + + Example 1.4. nghttp2_reply usage +... +event_route[nghttp2:request] { + nghttp2_reply("200", "OK", "text/html", + "OK"); +} +... + +5. Event Routes + + 5.1. nghttp2:request + +5.1. nghttp2:request + + The event route is executed when a new HTTP request is received. + + Inside it, the $nghttp2(...) group of variables is available, giving + access to several attributes of the HTTP request, such as method, URL, + data (body) or headers. +... +... +loadmodule "nghttp2.so +... +event_route[nghttp2:request] { + xinfo("request: $nghttp2(method) - url: $nghttp2(url) - data: [$nghttp2(data +)]\n"); + nghttp2_reply("200", "OK", "text/html", + "OK"); +} +... diff --git a/src/modules/nghttp2/doc/Makefile b/src/modules/nghttp2/doc/Makefile new file mode 100644 index 00000000000..023618181ec --- /dev/null +++ b/src/modules/nghttp2/doc/Makefile @@ -0,0 +1,4 @@ +docs = nghttp2.xml + +docbook_dir = ../../../../doc/docbook +include $(docbook_dir)/Makefile.module diff --git a/src/modules/nghttp2/doc/nghttp2.xml b/src/modules/nghttp2/doc/nghttp2.xml new file mode 100644 index 00000000000..9e4cd9aaef3 --- /dev/null +++ b/src/modules/nghttp2/doc/nghttp2.xml @@ -0,0 +1,36 @@ + + + +%docentities; + +]> + + + + NGHTTP2 Module + kamailio.org + + + Daniel-Constantin + Mierla + miconda@gmail.com + + + Daniel-Constantin + Mierla + miconda@gmail.com + + + + 2024 + asipto.com + + + + + + + diff --git a/src/modules/nghttp2/doc/nghttp2_admin.xml b/src/modules/nghttp2/doc/nghttp2_admin.xml new file mode 100644 index 00000000000..80465a7a49c --- /dev/null +++ b/src/modules/nghttp2/doc/nghttp2_admin.xml @@ -0,0 +1,180 @@ + + + +%docentities; + +]> + + + + + &adminguide; + +
+ Overview + + This module implements an embedded HTTP/2 server using nghttpd2 library. + +
+ +
+ Dependencies +
+ &kamailio; Modules + + The following modules must be loaded before this module: + + + + none. + + + + +
+
+ External Libraries or Applications + + The following libraries or applications must be installed before running + &kamailio; with this module loaded: + + + + libnghttp2 - libnghttpd library (v1.61.0+) + + + + +
+
+ +
+ Parameters +
+ <varname>listen_addr</varname> (str) + + IPv4 address to listen for HTTP2 connection. If not set, then it + listens on all local addresses (port has to be specified by + listen_port parameter). + + + + Default value is "" (empty - not set). + + + + Set <varname>listen_addr</varname> parameter + +... +modparam("nghttp2", "listen_addr", "127.0.0.1") +... + + +
+
+ <varname>listen_port</varname> (str) + + Port or service name to listen for HTTP2 connection. + + + + Default value is "8282". + + + + Set <varname>listen_port</varname> parameter + +... +modparam("nghttp2", "listen_port", "8284") +... + + +
+
+ <varname>event_callback</varname> (str) + + The name of the function in the kemi configuration file (embedded + scripting language such as Lua, Python, ...) to be executed instead + of event_route[nghttp2:request] block. + + + The function has one string parameter with the value "nghttp2:request". + + + + Default value is 'empty' (no function is executed for events). + + + + Set <varname>event_callback</varname> parameter + +... +modparam("nghttp2", "event_callback", "ksr_nghttp2_event") +... +-- event callback function implemented in Lua +function ksr_nghttp2_event(evname) + KSR.info("===== nghttp2 module triggered event: " .. evname .. "\n"); + return 1; +end +... + + +
+
+ +
+ Functions +
+ + <function moreinfo="none">nghttp2_reply(code, reason, ctype, body)</function> + + + Send back a reply with content-type and body. + + + <function>nghttp2_reply</function> usage + +... +event_route[nghttp2:request] { + nghttp2_reply("200", "OK", "text/html", + "<html><body>OK</body></html>"); +} +... + + +
+
+ +
+ Event Routes +
+ + <function moreinfo="none">nghttp2:request</function> + + + The event route is executed when a new HTTP request is received. + + + Inside it, the $nghttp2(...) group of variables is available, giving + access to several attributes of the HTTP request, such as method, + URL, data (body) or headers. + + +... +... +loadmodule "nghttp2.so +... +event_route[nghttp2:request] { + xinfo("request: $nghttp2(method) - url: $nghttp2(url) - data: [$nghttp2(data)]\n"); + nghttp2_reply("200", "OK", "text/html", + "<html><body>OK</body></html>"); +} +... + +
+
+ +
diff --git a/src/modules/nghttp2/nghttp2_mod.c b/src/modules/nghttp2/nghttp2_mod.c new file mode 100644 index 00000000000..b5b07a6d1a0 --- /dev/null +++ b/src/modules/nghttp2/nghttp2_mod.c @@ -0,0 +1,563 @@ +/** + * Copyright (C) 2024 Daniel-Constantin Mierla (asipto.com) + * + * This file is part of Kamailio, a free SIP server. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include +#include +#include + +#include "../../core/sr_module.h" +#include "../../core/dprint.h" +#include "../../core/ut.h" +#include "../../core/mod_fix.h" +#include "../../core/pvar.h" +#include "../../core/kemi.h" +#include "../../core/fmsg.h" +#include "../../core/cfg/cfg_struct.h" + +#include "nghttp2_server.h" + +MODULE_VERSION + +str _nghttp2_listen_port = str_init("8282"); +str _nghttp2_listen_addr = str_init(""); +str _nghttp2_tls_public_key = str_init(""); +str _nghttp2_tls_private_key = str_init(""); +int _nghttp2_server_pid = -1; + +static int nghttp2_route_no = -1; +static str nghttp2_event_callback = STR_NULL; + +static int w_nghttp2_send_reply( + sip_msg_t *msg, char *pcode, char *preason, char *pctype, char *pbody); + +static int fixup_nghttp2_send_reply(void **param, int param_no); + + +static int mod_init(void); +static int child_init(int); +static void mod_destroy(void); + +int pv_get_nghttp2(sip_msg_t *msg, pv_param_t *param, pv_value_t *res); +int pv_parse_nghttp2_name(pv_spec_p sp, str *in); + +/* clang-format off */ +static pv_export_t mod_pvs[] = { + {{"nghttp2", (sizeof("nghttp2") - 1)}, PVT_OTHER, pv_get_nghttp2, 0, + pv_parse_nghttp2_name, 0, 0, 0}, + + {{0, 0}, 0, 0, 0, 0, 0, 0, 0} +}; + +static cmd_export_t cmds[] = { + {"nghttp2_reply", (cmd_function)w_nghttp2_send_reply, + 4, fixup_nghttp2_send_reply, 0, REQUEST_ROUTE|EVENT_ROUTE}, + + {0, 0, 0, 0, 0, 0} +}; + +static param_export_t params[] = { + {"listen_port", PARAM_STR, &_nghttp2_listen_port}, + {"listen_addr", PARAM_STR, &_nghttp2_listen_addr}, + {"tls_public_key", PARAM_STR, &_nghttp2_tls_public_key}, + {"tls_private_key", PARAM_STR, &_nghttp2_tls_private_key}, + {"event_callback", PARAM_STR, &nghttp2_event_callback}, + {0, 0, 0} +}; + +struct module_exports exports = { + "nghttp2", /* module name */ + DEFAULT_DLFLAGS, /* dlopen flags */ + cmds, /* exported functions */ + params, /* exported parameters */ + 0, /* exported rpc functions */ + mod_pvs, /* exported pseudo-variables */ + 0, /* response handling function */ + mod_init, /* module init function */ + child_init, /* per child init function */ + mod_destroy /* destroy function */ +}; +/* clang-format on */ + + +/** + * init module function + */ +static int mod_init(void) +{ + sr_kemi_eng_t *keng = NULL; + int route_no = -1; + + if(nghttp2_event_callback.s != NULL && nghttp2_event_callback.len > 0) { + keng = sr_kemi_eng_get(); + if(keng == NULL) { + LM_ERR("failed to find kemi engine\n"); + return -1; + } + nghttp2_route_no = -1; + } else { + route_no = route_lookup(&event_rt, "nghttp2:request"); + if(route_no == -1) { + LM_ERR("failed to find event_route[nghttp2:request]\n"); + return -1; + } + if(event_rt.rlist[route_no] == 0) { + LM_WARN("event_route[nghttp2:request] is empty\n"); + } + nghttp2_route_no = route_no; + } + + /* add space for one extra process */ + register_procs(1); + + /* add child to update local config framework structures */ + cfg_register_child(1); + + return 0; +} + +/** + * @brief Initialize async module children + */ +static int child_init(int rank) +{ + int pid; + + if(rank != PROC_MAIN) + return 0; + + pid = fork_process(PROC_NOCHLDINIT, "NGHTTP2 Server Process", 1); + if(pid < 0) + return -1; /* error */ + if(pid == 0) { + /* child */ + _nghttp2_server_pid = getpid(); + + /* do child init to allow execution of rpc like functions */ + if(init_child(PROC_RPC) < 0) { + LM_DBG("failed to do RPC child init for dispatcher\n"); + return -1; + } + /* initialize the config framework */ + if(cfg_child_init()) + return -1; + if(nghttp2_server_run() < 0) { + LM_ERR("failed to initialize nghttp2 server process\n"); + return -1; + } + } + + return 0; +} + +/** + * destroy module function + */ +static void mod_destroy(void) +{ +} + +static ksr_nghttp2_ctx_t _ksr_nghttp2_ctx = {0}; + +/** + * parse the name of the $nghttp2(name) + */ +int pv_parse_nghttp2_name(pv_spec_p sp, str *in) +{ + if(sp == NULL || in == NULL || in->len <= 0) + return -1; + switch(in->len) { + case 3: + if(strncasecmp(in->s, "url", 3) == 0) { + sp->pvp.pvn.u.isname.name.n = 0; + } else { + goto error; + } + break; + case 4: + if(strncasecmp(in->s, "data", 4) == 0) { + sp->pvp.pvn.u.isname.name.n = 1; + } else if(strncasecmp(in->s, "size", 4) == 0) { + sp->pvp.pvn.u.isname.name.n = 2; + } else { + goto error; + } + break; + case 5: + if(strncasecmp(in->s, "srcip", 5) == 0) { + sp->pvp.pvn.u.isname.name.n = 5; + } else { + goto error; + } + break; + case 6: + if(strncasecmp(in->s, "method", 6) == 0) { + sp->pvp.pvn.u.isname.name.n = 3; + } else { + goto error; + } + break; + case 7: + if(strncasecmp(in->s, "version", 7) == 0) { + sp->pvp.pvn.u.isname.name.n = 4; + } else { + goto error; + } + break; + default: + if(in->len > 2 && in->s[1] == ':' + && (in->s[0] == 'h' || in->s[0] == 'H')) { + sp->pvp.pvn.type = PV_NAME_INTSTR; + sp->pvp.pvn.u.isname.type = PVT_HDR; + sp->pvp.pvn.u.isname.name.s = *in; + return 0; + } + goto error; + } + sp->pvp.pvn.type = PV_NAME_INTSTR; + sp->pvp.pvn.u.isname.type = 0; + + return 0; + +error: + LM_ERR("invalid variable name [%.*s]\n", in->len, in->s); + return -1; +} + +/** + * return the value of $nghttp2(name) + */ +int pv_get_nghttp2(sip_msg_t *msg, pv_param_t *param, pv_value_t *res) +{ + struct sockaddr *srcaddr = NULL; + const char *hdrval = NULL; + + if(param == NULL) { + return -1; + } + if(_ksr_nghttp2_ctx.connection == NULL) { + return pv_get_null(msg, param, res); + } + if(param->pvn.u.isname.type == PVT_HDR) { + //hdrval = MHD_lookup_connection_value(_ksr_mhttpd_ctx.connection, + // MHD_HEADER_KIND, param->pvn.u.isname.name.s.s + 2); + if(hdrval == NULL) { + return pv_get_null(msg, param, res); + } + return pv_get_strzval(msg, param, res, (char *)hdrval); + } + + switch(param->pvn.u.isname.name.n) { + case 0: /* url */ + return pv_get_strval(msg, param, res, &_ksr_nghttp2_ctx.url); + case 1: /* data */ + return pv_get_strval(msg, param, res, &_ksr_nghttp2_ctx.data); + case 2: /* size */ + return pv_get_sintval(msg, param, res, _ksr_nghttp2_ctx.data.len); + case 3: /* method */ + return pv_get_strval(msg, param, res, &_ksr_nghttp2_ctx.method); + case 4: /* version */ + return pv_get_strval( + msg, param, res, &_ksr_nghttp2_ctx.httpversion); + case 5: /* srcip */ + if(_ksr_nghttp2_ctx.srcip.len > 0) { + return pv_get_strval(msg, param, res, &_ksr_nghttp2_ctx.srcip); + } + srcaddr = NULL; + // (_ksr_nghttp2_ctx.cinfo ? _ksr_nghttp2_ctx.cinfo->client_addr + // : NULL); + if(srcaddr == NULL) { + return pv_get_null(msg, param, res); + } + switch(srcaddr->sa_family) { + case AF_INET: + if(!inet_ntop(AF_INET, + &(((struct sockaddr_in *)srcaddr)->sin_addr), + _ksr_nghttp2_ctx.srcipbuf, + IP_ADDR_MAX_STR_SIZE)) { + return pv_get_null(msg, param, res); + } + break; + case AF_INET6: + if(!inet_ntop(AF_INET6, + &(((struct sockaddr_in6 *)srcaddr)->sin6_addr), + _ksr_nghttp2_ctx.srcipbuf, + IP_ADDR_MAX_STR_SIZE)) { + return pv_get_null(msg, param, res); + } + break; + default: + return pv_get_null(msg, param, res); + } + _ksr_nghttp2_ctx.srcip.s = _ksr_nghttp2_ctx.srcipbuf; + _ksr_nghttp2_ctx.srcip.len = strlen(_ksr_nghttp2_ctx.srcipbuf); + return pv_get_strval(msg, param, res, &_ksr_nghttp2_ctx.srcip); + default: + return pv_get_null(msg, param, res); + } +} + +/** + * + */ +static int ksr_nghttp2_send_reply( + sip_msg_t *msg, int rcode, str *sreason, str *sctype, str *sbody) +{ + //struct MHD_Response *response; + // int ret; + + if(_ksr_nghttp2_ctx.connection == NULL) { + LM_ERR("no connection available\n"); + return -1; + } + + if(rcode < 100 || rcode >= 700) { + LM_ERR("invalid code parameter\n"); + return -1; + } + if(sreason->s == NULL || sreason->len == 0) { + LM_ERR("invalid reason parameter\n"); + return -1; + } + if(sctype->s == NULL) { + LM_ERR("invalid content-type parameter\n"); + return -1; + } + if(sbody->s == NULL) { + LM_ERR("invalid body parameter\n"); + return -1; + } + +#if 0 + response = MHD_create_response_from_buffer( + sbody->len, sbody->s, MHD_RESPMEM_PERSISTENT); + if(response == NULL) { + LM_ERR("failed to create the response\n"); + return -1; + } + if(sctype->len > 0) { + MHD_add_response_header(response, "Content-Type", sctype->s); + } + ret = MHD_queue_response( + _ksr_mhttpd_ctx.connection, (unsigned int)rcode, response); + MHD_destroy_response(response); + + return (ret == MHD_YES) ? 1 : -1; +#endif + return -1; +} + +/** + * + */ +static int w_nghttp2_send_reply( + sip_msg_t *msg, char *pcode, char *preason, char *pctype, char *pbody) +{ + str body = str_init(""); + str reason = str_init("OK"); + str ctype = str_init("text/plain"); + int code = 200; + + if(_ksr_nghttp2_ctx.connection == NULL) { + LM_ERR("no connection available\n"); + return -1; + } + + if(pcode == 0 || preason == 0 || pctype == 0 || pbody == 0) { + LM_ERR("invalid parameters\n"); + return -1; + } + + if(fixup_get_ivalue(msg, (gparam_p)pcode, &code) != 0) { + LM_ERR("no reply code value\n"); + return -1; + } + + if(fixup_get_svalue(msg, (gparam_p)preason, &reason) != 0) { + LM_ERR("unable to get reason\n"); + return -1; + } + + if(fixup_get_svalue(msg, (gparam_p)pctype, &ctype) != 0) { + LM_ERR("unable to get content type\n"); + return -1; + } + + if(fixup_get_svalue(msg, (gparam_p)pbody, &body) != 0) { + LM_ERR("unable to get body\n"); + return -1; + } + + return ksr_nghttp2_send_reply(msg, code, &reason, &ctype, &body); +} + +static int fixup_nghttp2_send_reply(void **param, int param_no) +{ + if(param_no == 1) { + return fixup_igp_null(param, 1); + } else if(param_no == 2) { + return fixup_spve_null(param, 1); + } else if(param_no == 3) { + return fixup_spve_null(param, 1); + } else if(param_no == 4) { + return fixup_spve_null(param, 1); + } + return 0; +} + +#if 0 +static enum MHD_Result ksr_microhttpd_request(void *cls, + struct MHD_Connection *connection, const char *url, const char *method, + const char *version, const char *upload_data, size_t *upload_data_size, + void **ptr) +{ + static int _first_callback; + sr_kemi_eng_t *keng = NULL; + str evname = str_init("microhttpd:request"); + sip_msg_t *fmsg = NULL; + run_act_ctx_t ctx; + int rtb; + + if(&_first_callback != *ptr) { + /* the first time only the headers are valid, + do not respond in the first round... */ + *ptr = &_first_callback; + return MHD_YES; + } + *ptr = NULL; /* clear context pointer */ + + _ksr_mhttpd_ctx.connection = connection; + _ksr_mhttpd_ctx.method.s = (char *)method; + _ksr_mhttpd_ctx.method.len = strlen(_ksr_mhttpd_ctx.method.s); + _ksr_mhttpd_ctx.url.s = (char *)url; + _ksr_mhttpd_ctx.url.len = strlen(_ksr_mhttpd_ctx.url.s); + _ksr_mhttpd_ctx.httpversion.s = (char *)version; + _ksr_mhttpd_ctx.httpversion.len = strlen(_ksr_mhttpd_ctx.httpversion.s); + if(*upload_data_size > 0) { + _ksr_mhttpd_ctx.data.s = (char *)upload_data; + _ksr_mhttpd_ctx.data.len = (int)(*upload_data_size); + } else { + _ksr_mhttpd_ctx.data.s = NULL; + _ksr_mhttpd_ctx.data.len = 0; + } + _ksr_mhttpd_ctx.cinfo = MHD_get_connection_info( + connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS); + _ksr_mhttpd_ctx.srcip.s = NULL; + _ksr_mhttpd_ctx.srcip.len = 0; + + LM_DBG("executing event_route[%s] (%d)\n", evname.s, microhttpd_route_no); + if(faked_msg_init() < 0) { + return MHD_NO; + } + fmsg = faked_msg_next(); + rtb = get_route_type(); + set_route_type(REQUEST_ROUTE); + init_run_actions_ctx(&ctx); + if(microhttpd_route_no >= 0) { + run_top_route(event_rt.rlist[microhttpd_route_no], fmsg, &ctx); + } else { + keng = sr_kemi_eng_get(); + if(keng != NULL) { + if(sr_kemi_ctx_route(keng, &ctx, fmsg, EVENT_ROUTE, + µhttpd_event_callback, &evname) + < 0) { + LM_ERR("error running event route kemi callback\n"); + return MHD_NO; + } + } + } + set_route_type(rtb); + if(ctx.run_flags & DROP_R_F) { + LM_ERR("exit due to 'drop' in event route\n"); + return MHD_NO; + } + + return MHD_YES; +} + +#define KSR_MICROHTTPD_PAGE \ + "Kamailio" \ + "Thanks for flying Kamailio!" +/** + * + */ +static int microhttpd_server_run(void) +{ + + struct MHD_Daemon *d; + struct sockaddr_in address; + + if(_microhttpd_listen_addr.len > 0) { + address.sin_family = AF_INET; + address.sin_port = htons(_microhttpd_listen_port); + if(inet_pton(AF_INET, _microhttpd_listen_addr.s, &address.sin_addr) + <= 0) { + LM_ERR("failed to convert listen address\n"); + return -1; + } + LM_DBG("preparing to listen on %s :%d\n", _microhttpd_listen_addr.s, + _microhttpd_listen_port); + d = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, _microhttpd_listen_port, + NULL, NULL, &ksr_microhttpd_request, KSR_MICROHTTPD_PAGE, + MHD_OPTION_SOCK_ADDR, &address, MHD_OPTION_END); + } else { + LM_DBG("preparing to listen on port: %d\n", _microhttpd_listen_port); + d = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY, _microhttpd_listen_port, + NULL, NULL, &ksr_microhttpd_request, KSR_MICROHTTPD_PAGE, + MHD_OPTION_END); + } + + if(d == NULL) { + return -1; + } + while(1) { + sleep(10); + } + return 0; +} +#endif + +/** + * + */ +/* clang-format off */ +static sr_kemi_t sr_kemi_nghttp2_exports[] = { + { str_init("nghttp2"), str_init("nghttp2_reply"), + SR_KEMIP_INT, ksr_nghttp2_send_reply, + { SR_KEMIP_INT, SR_KEMIP_STR, SR_KEMIP_STR, + SR_KEMIP_STR, SR_KEMIP_NONE, SR_KEMIP_NONE } + }, + + { {0, 0}, {0, 0}, 0, NULL, { 0, 0, 0, 0, 0, 0 } } +}; +/* clang-format on */ + + +/** + * + */ +int mod_register(char *path, int *dlflags, void *p1, void *p2) +{ + sr_kemi_modules_add(sr_kemi_nghttp2_exports); + return 0; +} diff --git a/src/modules/nghttp2/nghttp2_server.c b/src/modules/nghttp2/nghttp2_server.c new file mode 100644 index 00000000000..7c8d900987c --- /dev/null +++ b/src/modules/nghttp2/nghttp2_server.c @@ -0,0 +1,760 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#define OUTPUT_WOULDBLOCK_THRESHOLD (1 << 16) + +#define ARRLEN(x) (sizeof(x) / sizeof(x[0])) + +#define MAKE_NV(NAME, VALUE) \ + { \ + (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, \ + sizeof(VALUE) - 1, NGHTTP2_NV_FLAG_NONE \ + } + +#include "nghttp2_server.h" + +struct app_context; +typedef struct app_context app_context; + +typedef struct http2_stream_data +{ + struct http2_stream_data *prev, *next; + char *request_path; + int32_t stream_id; + int fd; +} http2_stream_data; + +typedef struct http2_session_data +{ + struct http2_stream_data root; + struct bufferevent *bev; + app_context *app_ctx; + nghttp2_session *session; + char *client_addr; +} http2_session_data; + +struct app_context +{ + SSL_CTX *ssl_ctx; + struct event_base *evbase; +}; + +static int alpn_select_proto_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, unsigned int inlen, + void *arg) +{ + int rv; + (void)ssl; + (void)arg; + + rv = nghttp2_select_alpn(out, outlen, in, inlen); + + if(rv != 1) { + return SSL_TLSEXT_ERR_NOACK; + } + + return SSL_TLSEXT_ERR_OK; +} + +/* Create SSL_CTX. */ +static SSL_CTX *create_ssl_ctx(const char *key_file, const char *cert_file) +{ + SSL_CTX *ssl_ctx; + + ssl_ctx = SSL_CTX_new(TLS_server_method()); + if(!ssl_ctx) { + LM_ERR("Could not create SSL/TLS context: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + SSL_CTX_set_options( + ssl_ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 + | SSL_OP_NO_COMPRESSION + | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + if(SSL_CTX_set1_curves_list(ssl_ctx, "P-256") != 1) { + LM_ERR("SSL_CTX_set1_curves_list failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + } +#else /* !(OPENSSL_VERSION_NUMBER >= 0x30000000L) */ + { + EC_KEY *ecdh; + ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if(!ecdh) { + LM_ERR("EC_KEY_new_by_curv_name failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + SSL_CTX_set_tmp_ecdh(ssl_ctx, ecdh); + EC_KEY_free(ecdh); + } +#endif /* !(OPENSSL_VERSION_NUMBER >= 0x30000000L) */ + + if(SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM) != 1) { + LM_ERR("Could not read private key file %s", key_file); + } + if(SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_file) != 1) { + LM_ERR("Could not read certificate file %s", cert_file); + } + + SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, NULL); + + return ssl_ctx; +} + +/* Create SSL object */ +static SSL *create_ssl(SSL_CTX *ssl_ctx) +{ + SSL *ssl; + ssl = SSL_new(ssl_ctx); + if(!ssl) { + LM_ERR("Could not create SSL/TLS session object: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + return ssl; +} + +static void add_stream( + http2_session_data *session_data, http2_stream_data *stream_data) +{ + stream_data->next = session_data->root.next; + session_data->root.next = stream_data; + stream_data->prev = &session_data->root; + if(stream_data->next) { + stream_data->next->prev = stream_data; + } +} + +static void remove_stream( + http2_session_data *session_data, http2_stream_data *stream_data) +{ + (void)session_data; + + stream_data->prev->next = stream_data->next; + if(stream_data->next) { + stream_data->next->prev = stream_data->prev; + } +} + +static http2_stream_data *create_http2_stream_data( + http2_session_data *session_data, int32_t stream_id) +{ + http2_stream_data *stream_data; + stream_data = malloc(sizeof(http2_stream_data)); + memset(stream_data, 0, sizeof(http2_stream_data)); + stream_data->stream_id = stream_id; + stream_data->fd = -1; + + add_stream(session_data, stream_data); + return stream_data; +} + +static void delete_http2_stream_data(http2_stream_data *stream_data) +{ + if(stream_data->fd != -1) { + close(stream_data->fd); + } + free(stream_data->request_path); + free(stream_data); +} + +static http2_session_data *create_http2_session_data( + app_context *app_ctx, int fd, struct sockaddr *addr, int addrlen) +{ + int rv; + http2_session_data *session_data; + SSL *ssl; + char host[NI_MAXHOST]; + int val = 1; + + ssl = create_ssl(app_ctx->ssl_ctx); + session_data = malloc(sizeof(http2_session_data)); + memset(session_data, 0, sizeof(http2_session_data)); + session_data->app_ctx = app_ctx; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&val, sizeof(val)); + session_data->bev = bufferevent_openssl_socket_new(app_ctx->evbase, fd, ssl, + BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + bufferevent_enable(session_data->bev, EV_READ | EV_WRITE); + rv = getnameinfo(addr, (socklen_t)addrlen, host, sizeof(host), NULL, 0, + NI_NUMERICHOST); + if(rv != 0) { + session_data->client_addr = strdup("(unknown)"); + } else { + session_data->client_addr = strdup(host); + } + + return session_data; +} + +static void delete_http2_session_data(http2_session_data *session_data) +{ + http2_stream_data *stream_data; + SSL *ssl = bufferevent_openssl_get_ssl(session_data->bev); + fprintf(stderr, "%s disconnected\n", session_data->client_addr); + if(ssl) { + SSL_shutdown(ssl); + } + bufferevent_free(session_data->bev); + nghttp2_session_del(session_data->session); + for(stream_data = session_data->root.next; stream_data;) { + http2_stream_data *next = stream_data->next; + delete_http2_stream_data(stream_data); + stream_data = next; + } + free(session_data->client_addr); + free(session_data); +} + +/* Serialize the frame and send (or buffer) the data to + bufferevent. */ +static int session_send(http2_session_data *session_data) +{ + int rv; + rv = nghttp2_session_send(session_data->session); + if(rv != 0) { + LM_ERR("Fatal error: %s", nghttp2_strerror(rv)); + return -1; + } + return 0; +} + +/* Read the data in the bufferevent and feed them into nghttp2 library + function. Invocation of nghttp2_session_mem_recv2() may make + additional pending frames, so call session_send() at the end of the + function. */ +static int session_recv(http2_session_data *session_data) +{ + nghttp2_ssize readlen; + struct evbuffer *input = bufferevent_get_input(session_data->bev); + size_t datalen = evbuffer_get_length(input); + unsigned char *data = evbuffer_pullup(input, -1); + + readlen = nghttp2_session_mem_recv2(session_data->session, data, datalen); + if(readlen < 0) { + LM_ERR("Fatal error: %s", nghttp2_strerror((int)readlen)); + return -1; + } + if(evbuffer_drain(input, (size_t)readlen) != 0) { + LM_ERR("Fatal error: evbuffer_drain failed"); + return -1; + } + if(session_send(session_data) != 0) { + return -1; + } + return 0; +} + +static nghttp2_ssize send_callback(nghttp2_session *session, + const uint8_t *data, size_t length, int flags, void *user_data) +{ + http2_session_data *session_data = (http2_session_data *)user_data; + struct bufferevent *bev = session_data->bev; + (void)session; + (void)flags; + + /* Avoid excessive buffering in server side. */ + if(evbuffer_get_length(bufferevent_get_output(session_data->bev)) + >= OUTPUT_WOULDBLOCK_THRESHOLD) { + return NGHTTP2_ERR_WOULDBLOCK; + } + bufferevent_write(bev, data, length); + return (nghttp2_ssize)length; +} + +/* Returns nonzero if the string |s| ends with the substring |sub| */ +static int ends_with(const char *s, const char *sub) +{ + size_t slen = strlen(s); + size_t sublen = strlen(sub); + if(slen < sublen) { + return 0; + } + return memcmp(s + slen - sublen, sub, sublen) == 0; +} + +/* Returns int value of hex string character |c| */ +static uint8_t hex_to_uint(uint8_t c) +{ + if('0' <= c && c <= '9') { + return (uint8_t)(c - '0'); + } + if('A' <= c && c <= 'F') { + return (uint8_t)(c - 'A' + 10); + } + if('a' <= c && c <= 'f') { + return (uint8_t)(c - 'a' + 10); + } + return 0; +} + +/* Decodes percent-encoded byte string |value| with length |valuelen| + and returns the decoded byte string in allocated buffer. The return + value is NULL terminated. The caller must free the returned + string. */ +static char *percent_decode(const uint8_t *value, size_t valuelen) +{ + char *res; + + res = malloc(valuelen + 1); + if(valuelen > 3) { + size_t i, j; + for(i = 0, j = 0; i < valuelen - 2;) { + if(value[i] != '%' || !isxdigit(value[i + 1]) + || !isxdigit(value[i + 2])) { + res[j++] = (char)value[i++]; + continue; + } + res[j++] = (char)((hex_to_uint(value[i + 1]) << 4) + + hex_to_uint(value[i + 2])); + i += 3; + } + memcpy(&res[j], &value[i], 2); + res[j + 2] = '\0'; + } else { + memcpy(res, value, valuelen); + res[valuelen] = '\0'; + } + return res; +} + +static nghttp2_ssize file_read_callback(nghttp2_session *session, + int32_t stream_id, uint8_t *buf, size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) +{ + int fd = source->fd; + ssize_t r; + (void)session; + (void)stream_id; + (void)user_data; + + while((r = read(fd, buf, length)) == -1 && errno == EINTR) + ; + if(r == -1) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + if(r == 0) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + return (nghttp2_ssize)r; +} + +static int send_response(nghttp2_session *session, int32_t stream_id, + nghttp2_nv *nva, size_t nvlen, int fd) +{ + int rv; + nghttp2_data_provider2 data_prd; + data_prd.source.fd = fd; + data_prd.read_callback = file_read_callback; + + rv = nghttp2_submit_response2(session, stream_id, nva, nvlen, &data_prd); + if(rv != 0) { + LM_ERR("Fatal error: %s", nghttp2_strerror(rv)); + return -1; + } + return 0; +} + +static const char ERROR_HTML[] = "404" + "

404 Not Found

"; + +static int error_reply(nghttp2_session *session, http2_stream_data *stream_data) +{ + int rv; + ssize_t writelen; + int pipefd[2]; + nghttp2_nv hdrs[] = {MAKE_NV(":status", "404")}; + + rv = pipe(pipefd); + if(rv != 0) { + LM_ERR("Could not create pipe"); + rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + stream_data->stream_id, NGHTTP2_INTERNAL_ERROR); + if(rv != 0) { + LM_ERR("Fatal error: %s", nghttp2_strerror(rv)); + return -1; + } + return 0; + } + + writelen = write(pipefd[1], ERROR_HTML, sizeof(ERROR_HTML) - 1); + close(pipefd[1]); + + if(writelen != sizeof(ERROR_HTML) - 1) { + close(pipefd[0]); + return -1; + } + + stream_data->fd = pipefd[0]; + + if(send_response( + session, stream_data->stream_id, hdrs, ARRLEN(hdrs), pipefd[0]) + != 0) { + close(pipefd[0]); + return -1; + } + return 0; +} + +/* nghttp2_on_header_callback: Called when nghttp2 library emits + single header name/value pair. */ +static int on_header_callback(nghttp2_session *session, + const nghttp2_frame *frame, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, uint8_t flags, void *user_data) +{ + http2_stream_data *stream_data; + const char PATH[] = ":path"; + (void)flags; + (void)user_data; + + switch(frame->hd.type) { + case NGHTTP2_HEADERS: + if(frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + break; + } + stream_data = nghttp2_session_get_stream_user_data( + session, frame->hd.stream_id); + if(!stream_data || stream_data->request_path) { + break; + } + if(namelen == sizeof(PATH) - 1 + && memcmp(PATH, name, namelen) == 0) { + size_t j; + for(j = 0; j < valuelen && value[j] != '?'; ++j) + ; + stream_data->request_path = percent_decode(value, j); + } + break; + } + return 0; +} + +static int on_begin_headers_callback( + nghttp2_session *session, const nghttp2_frame *frame, void *user_data) +{ + http2_session_data *session_data = (http2_session_data *)user_data; + http2_stream_data *stream_data; + + if(frame->hd.type != NGHTTP2_HEADERS + || frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + stream_data = create_http2_stream_data(session_data, frame->hd.stream_id); + nghttp2_session_set_stream_user_data( + session, frame->hd.stream_id, stream_data); + return 0; +} + +/* Minimum check for directory traversal. Returns nonzero if it is + safe. */ +static int check_path(const char *path) +{ + /* We don't like '\' in url. */ + return path[0] && path[0] == '/' && strchr(path, '\\') == NULL + && strstr(path, "/../") == NULL && strstr(path, "/./") == NULL + && !ends_with(path, "/..") && !ends_with(path, "/."); +} + +static int on_request_recv(nghttp2_session *session, + http2_session_data *session_data, http2_stream_data *stream_data) +{ + int fd; + nghttp2_nv hdrs[] = {MAKE_NV(":status", "200")}; + char *rel_path; + + if(!stream_data->request_path) { + if(error_reply(session, stream_data) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + fprintf(stderr, "%s GET %s\n", session_data->client_addr, + stream_data->request_path); + if(!check_path(stream_data->request_path)) { + if(error_reply(session, stream_data) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + for(rel_path = stream_data->request_path; *rel_path == '/'; ++rel_path) + ; + fd = open(rel_path, O_RDONLY); + if(fd == -1) { + if(error_reply(session, stream_data) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + stream_data->fd = fd; + + if(send_response(session, stream_data->stream_id, hdrs, ARRLEN(hdrs), fd) + != 0) { + close(fd); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +static int on_frame_recv_callback( + nghttp2_session *session, const nghttp2_frame *frame, void *user_data) +{ + http2_session_data *session_data = (http2_session_data *)user_data; + http2_stream_data *stream_data; + switch(frame->hd.type) { + case NGHTTP2_DATA: + case NGHTTP2_HEADERS: + /* Check that the client request has finished */ + if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + stream_data = nghttp2_session_get_stream_user_data( + session, frame->hd.stream_id); + /* For DATA and HEADERS frame, this callback may be called after + on_stream_close_callback. Check that stream still alive. */ + if(!stream_data) { + return 0; + } + return on_request_recv(session, session_data, stream_data); + } + break; + default: + break; + } + return 0; +} + +static int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) +{ + http2_session_data *session_data = (http2_session_data *)user_data; + http2_stream_data *stream_data; + (void)error_code; + + stream_data = nghttp2_session_get_stream_user_data(session, stream_id); + if(!stream_data) { + return 0; + } + remove_stream(session_data, stream_data); + delete_http2_stream_data(stream_data); + return 0; +} + +static void initialize_nghttp2_session(http2_session_data *session_data) +{ + nghttp2_session_callbacks *callbacks; + + nghttp2_session_callbacks_new(&callbacks); + + nghttp2_session_callbacks_set_send_callback2(callbacks, send_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback( + callbacks, on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_header_callback( + callbacks, on_header_callback); + + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + + nghttp2_session_server_new(&session_data->session, callbacks, session_data); + + nghttp2_session_callbacks_del(callbacks); +} + +/* Send HTTP/2 client connection header, which includes 24 bytes + magic octets and SETTINGS frame */ +static int send_server_connection_header(http2_session_data *session_data) +{ + nghttp2_settings_entry iv[1] = { + {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}}; + int rv; + + rv = nghttp2_submit_settings( + session_data->session, NGHTTP2_FLAG_NONE, iv, ARRLEN(iv)); + if(rv != 0) { + LM_ERR("Fatal error: %s", nghttp2_strerror(rv)); + return -1; + } + return 0; +} + +/* readcb for bufferevent after client connection header was + checked. */ +static void readcb(struct bufferevent *bev, void *ptr) +{ + http2_session_data *session_data = (http2_session_data *)ptr; + (void)bev; + + if(session_recv(session_data) != 0) { + delete_http2_session_data(session_data); + return; + } +} + +/* writecb for bufferevent. To greaceful shutdown after sending or + receiving GOAWAY, we check the some conditions on the nghttp2 + library and output buffer of bufferevent. If it indicates we have + no business to this session, tear down the connection. If the + connection is not going to shutdown, we call session_send() to + process pending data in the output buffer. This is necessary + because we have a threshold on the buffer size to avoid too much + buffering. See send_callback(). */ +static void writecb(struct bufferevent *bev, void *ptr) +{ + http2_session_data *session_data = (http2_session_data *)ptr; + if(evbuffer_get_length(bufferevent_get_output(bev)) > 0) { + return; + } + if(nghttp2_session_want_read(session_data->session) == 0 + && nghttp2_session_want_write(session_data->session) == 0) { + delete_http2_session_data(session_data); + return; + } + if(session_send(session_data) != 0) { + delete_http2_session_data(session_data); + return; + } +} + +/* eventcb for bufferevent */ +static void eventcb(struct bufferevent *bev, short events, void *ptr) +{ + http2_session_data *session_data = (http2_session_data *)ptr; + if(events & BEV_EVENT_CONNECTED) { + const unsigned char *alpn = NULL; + unsigned int alpnlen = 0; + SSL *ssl; + (void)bev; + + LM_ERR("%s connected\n", session_data->client_addr); + + ssl = bufferevent_openssl_get_ssl(session_data->bev); + + SSL_get0_alpn_selected(ssl, &alpn, &alpnlen); + + if(alpn == NULL || alpnlen != 2 || memcmp("h2", alpn, 2) != 0) { + LM_ERR("%s h2 is not negotiated\n", session_data->client_addr); + delete_http2_session_data(session_data); + return; + } + + initialize_nghttp2_session(session_data); + + if(send_server_connection_header(session_data) != 0 + || session_send(session_data) != 0) { + delete_http2_session_data(session_data); + return; + } + + return; + } + if(events & BEV_EVENT_EOF) { + LM_ERR("%s EOF\n", session_data->client_addr); + } else if(events & BEV_EVENT_ERROR) { + LM_ERR("%s network error\n", session_data->client_addr); + } else if(events & BEV_EVENT_TIMEOUT) { + LM_ERR("%s timeout\n", session_data->client_addr); + } + delete_http2_session_data(session_data); +} + +/* callback for evconnlistener */ +static void acceptcb(struct evconnlistener *listener, int fd, + struct sockaddr *addr, int addrlen, void *arg) +{ + app_context *app_ctx = (app_context *)arg; + http2_session_data *session_data; + (void)listener; + + session_data = create_http2_session_data(app_ctx, fd, addr, addrlen); + + bufferevent_setcb( + session_data->bev, readcb, writecb, eventcb, session_data); +} + +static void start_listen(struct event_base *evbase, app_context *app_ctx) +{ + int rv; + struct addrinfo hints; + struct addrinfo *res, *rp; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; +#ifdef AI_ADDRCONFIG + hints.ai_flags |= AI_ADDRCONFIG; +#endif /* AI_ADDRCONFIG */ + + rv = getaddrinfo( + _nghttp2_listen_addr.s, _nghttp2_listen_port.s, &hints, &res); + if(rv != 0) { + LM_ERR("Could not resolve server address"); + } + for(rp = res; rp; rp = rp->ai_next) { + struct evconnlistener *listener; + listener = evconnlistener_new_bind(evbase, acceptcb, app_ctx, + LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 16, rp->ai_addr, + (int)rp->ai_addrlen); + if(listener) { + freeaddrinfo(res); + + return; + } + } + LM_ERR("Could not start listener"); +} + +static void initialize_app_context( + app_context *app_ctx, SSL_CTX *ssl_ctx, struct event_base *evbase) +{ + memset(app_ctx, 0, sizeof(app_context)); + app_ctx->ssl_ctx = ssl_ctx; + app_ctx->evbase = evbase; +} + +int nghttp2_server_run(void) +{ + SSL_CTX *ssl_ctx; + app_context app_ctx; + struct event_base *evbase; + struct sigaction act; + + memset(&act, 0, sizeof(struct sigaction)); + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, NULL); + + ssl_ctx = create_ssl_ctx( + _nghttp2_tls_private_key.s, _nghttp2_tls_public_key.s); + evbase = event_base_new(); + initialize_app_context(&app_ctx, ssl_ctx, evbase); + start_listen(evbase, &app_ctx); + + event_base_loop(evbase, 0); + + event_base_free(evbase); + SSL_CTX_free(ssl_ctx); + + return 0; +} diff --git a/src/modules/nghttp2/nghttp2_server.h b/src/modules/nghttp2/nghttp2_server.h new file mode 100644 index 00000000000..8570f6fd914 --- /dev/null +++ b/src/modules/nghttp2/nghttp2_server.h @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2024 Daniel-Constantin Mierla (asipto.com) + * + * This file is part of Kamailio, a free SIP server. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef NGHTTP2_SERVER_H_ +#define NGHTTP2_SERVER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#define NGHTTP2_NO_SSIZE_T +#include + +#include "../../core/str.h" +#include "../../core/ip_addr.h" + +typedef ptrdiff_t nghttp2_ssize; + +typedef struct ksr_nghttp2_ctx +{ + //struct MHD_Connection *connection; + void *connection; + str method; + str url; + str httpversion; + str data; + //const union MHD_ConnectionInfo *cinfo; + void *cinfo; + char srcipbuf[IP_ADDR_MAX_STR_SIZE]; + str srcip; +} ksr_nghttp2_ctx_t; + +extern str _nghttp2_listen_port; +extern str _nghttp2_listen_addr; +extern str _nghttp2_tls_public_key; +extern str _nghttp2_tls_private_key; +extern int _nghttp2_server_pid; + +int nghttp2_server_run(void); + +#endif