From 2397fb8a496e41287053756956d9fbd58c12fec1 Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Mon, 6 Jun 2022 07:51:05 +0200 Subject: [PATCH] Split 'websocket' module up in 'webserver' and 'websocket' --- Makefile.windows | 4 + doc/conf/modules.optional.conf | 3 + include/h.h | 4 + include/modules.h | 2 + include/struct.h | 29 +++ src/api-efunctions.c | 4 + src/misc.c | 8 + src/modules/Makefile.in | 2 +- src/modules/webserver.c | 457 +++++++++++++++++++++++++++++++++ src/modules/websocket.c | 306 ++++------------------ 10 files changed, 569 insertions(+), 250 deletions(-) create mode 100644 src/modules/webserver.c diff --git a/Makefile.windows b/Makefile.windows index f0f88981d..845b92cc6 100644 --- a/Makefile.windows +++ b/Makefile.windows @@ -407,6 +407,7 @@ DLL_FILES=\ src/modules/watch.dll \ src/modules/webirc.dll \ src/modules/webredir.dll \ + src/modules/webserver.dll \ src/modules/websocket.dll \ src/modules/whois.dll \ src/modules/who_old.dll \ @@ -1297,6 +1298,9 @@ src/modules/webirc.dll: src/modules/webirc.c $(INCLUDES) src/modules/webredir.dll: src/modules/webredir.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/webredir.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/webredir.pdb $(MODLFLAGS) +src/modules/webserver.dll: src/modules/webserver.c $(INCLUDES) + $(CC) $(MODCFLAGS) src/modules/webserver.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/webserver.pdb $(MODLFLAGS) + src/modules/websocket.dll: src/modules/websocket.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/websocket.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/websocket.pdb $(MODLFLAGS) diff --git a/doc/conf/modules.optional.conf b/doc/conf/modules.optional.conf index 4dff17f02..8b3715387 100644 --- a/doc/conf/modules.optional.conf +++ b/doc/conf/modules.optional.conf @@ -141,6 +141,9 @@ set { // } //} +// Load the webserver module, needed for websocket (see next) +loadmodule "webserver"; + // This adds websocket support. For more information, see: // https://www.unrealircd.org/docs/WebSocket_support loadmodule "websocket"; diff --git a/include/h.h b/include/h.h index d14120676..aedd31b1e 100644 --- a/include/h.h +++ b/include/h.h @@ -836,6 +836,8 @@ extern MODVAR char *(*get_chmodes_for_user)(Client *client, const char *flags); extern MODVAR WhoisConfigDetails (*whois_get_policy)(Client *client, Client *target, const char *name); extern MODVAR int (*make_oper)(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost); extern MODVAR int (*unreal_match_iplist)(Client *client, NameList *l); +extern MODVAR void (*webserver_send_response)(Client *client, int status, char *msg); +extern MODVAR void (*webserver_close_client)(Client *client); /* /Efuncs */ /* TLS functions */ @@ -872,6 +874,8 @@ extern int del_silence_default_handler(Client *client, const char *mask); extern int is_silenced_default_handler(Client *client, Client *acptr); extern void do_unreal_log_remote_deliver_default_handler(LogLevel loglevel, const char *subsystem, const char *event_id, MultiLine *msg, const char *json_serialized); extern int make_oper_default_handler(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost); +extern void webserver_send_response_default_handler(Client *client, int status, char *msg); +extern void webserver_close_client_default_handler(Client *client); /* End of default handlers for efunctions */ extern MODVAR MOTDFile opermotd, svsmotd, motd, botmotd, smotd, rules; diff --git a/include/modules.h b/include/modules.h index af1db278e..bc6971f39 100644 --- a/include/modules.h +++ b/include/modules.h @@ -2442,6 +2442,8 @@ enum EfunctionType { EFUNC_WHOIS_GET_POLICY, EFUNC_MAKE_OPER, EFUNC_UNREAL_MATCH_IPLIST, + EFUNC_WEBSERVER_SEND_RESPONSE, + EFUNC_WEBSERVER_CLOSE_CLIENT, }; /* Module flags */ diff --git a/include/struct.h b/include/struct.h index 86d99e60b..77c9a15d6 100644 --- a/include/struct.h +++ b/include/struct.h @@ -1693,6 +1693,34 @@ struct ConfigItem_tld { u_short options; }; +#define WEB_OPT_ENABLE 0x1 + +typedef enum HttpMethod { + HTTP_METHOD_NONE = 0, /**< No valid HTTP request (yet) */ + HTTP_METHOD_HEAD = 1, /**< HEAD request */ + HTTP_METHOD_GET = 2, /**< GET request */ + HTTP_METHOD_PUT = 3, /**< PUT request */ + HTTP_METHOD_POST = 4, /**< POST request */ +} HttpMethod; + +typedef struct WebRequest WebRequest; +struct WebRequest { + HttpMethod method; /**< GET/PUT/POST */ + char *uri; /**< Requested resource, eg "/api" */ + NameValuePrioList *headers; /**< HTTP request headers */ + int num_headers; /**< Number of HTTP request headers (also used for sorting the list) */ + char request_header_parsed; /**< Done parsing? */ + char *lefttoparse; /**< Leftover buffer to parse */ + int lefttoparselen; /**< Length of lefttoparse buffer */ + int pending_close; /**< Set to 1 when connection should be closed as soon as all data is sent (sendq==0) */ +}; + +typedef struct WebServer WebServer; +struct WebServer { + int (*handle_request)(Client *client, WebRequest *web); + int (*handle_data)(Client *client, WebRequest *web, const char *buf, int length); +}; + struct ConfigItem_listen { ConfigItem_listen *prev, *next; ConfigFlag flag; @@ -1704,6 +1732,7 @@ struct ConfigItem_listen { int fd; SSL_CTX *ssl_ctx; TLSOptions *tls_options; + WebServer *webserver; int websocket_options; /* should be in module, but lazy */ char *websocket_forward; }; diff --git a/src/api-efunctions.c b/src/api-efunctions.c index 5bf877dd4..7e02bd499 100644 --- a/src/api-efunctions.c +++ b/src/api-efunctions.c @@ -137,6 +137,8 @@ char *(*get_chmodes_for_user)(Client *client, const char *flags); WhoisConfigDetails (*whois_get_policy)(Client *client, Client *target, const char *name); int (*make_oper)(Client *client, const char *operblock_name, const char *operclass, ConfigItem_class *clientclass, long modes, const char *snomask, const char *vhost); int (*unreal_match_iplist)(Client *client, NameList *l); +void (*webserver_send_response)(Client *client, int status, char *msg); +void (*webserver_close_client)(Client *client); Efunction *EfunctionAddMain(Module *module, EfunctionType eftype, int (*func)(), void (*vfunc)(), void *(*pvfunc)(), char *(*stringfunc)(), const char *(*conststringfunc)()) { @@ -409,4 +411,6 @@ void efunctions_init(void) efunc_init_function(EFUNC_WHOIS_GET_POLICY, whois_get_policy, NULL); efunc_init_function(EFUNC_MAKE_OPER, make_oper, make_oper_default_handler); efunc_init_function(EFUNC_UNREAL_MATCH_IPLIST, unreal_match_iplist, NULL); + efunc_init_function(EFUNC_WEBSERVER_SEND_RESPONSE, webserver_send_response, webserver_send_response_default_handler); + efunc_init_function(EFUNC_WEBSERVER_CLOSE_CLIENT, webserver_close_client, webserver_close_client_default_handler); } diff --git a/src/misc.c b/src/misc.c index 350101bae..f8922010b 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1395,6 +1395,14 @@ int make_oper_default_handler(Client *client, const char *operblock_name, const return 0; } +extern void webserver_send_response_default_handler(Client *client, int status, char *msg) +{ +} + +extern void webserver_close_client_default_handler(Client *client) +{ +} + /** my_timegm: mktime()-like function which will use GMT/UTC. * Strangely enough there is no standard function for this. * On some *NIX OS's timegm() may be available, sometimes only diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in index fce20a4a2..97cb4803a 100644 --- a/src/modules/Makefile.in +++ b/src/modules/Makefile.in @@ -59,7 +59,7 @@ MODULES= \ botmotd.so lusers.so names.so svsnolag.so addmotd.so \ svslusers.so starttls.so webredir.so cap.so \ sasl.so md.so certfp.so \ - tls_antidos.so webirc.so websocket.so \ + tls_antidos.so webirc.so webserver.so websocket.so \ blacklist.so jointhrottle.so \ antirandom.so hideserver.so jumpserver.so \ ircops.so staff.so nocodes.so \ diff --git a/src/modules/webserver.c b/src/modules/webserver.c new file mode 100644 index 000000000..6190c0eb7 --- /dev/null +++ b/src/modules/webserver.c @@ -0,0 +1,457 @@ +/* + * Webserver + * (C)Copyright 2016 Bram Matthys and the UnrealIRCd team + * License: GPLv2 or later + */ + +#include "unrealircd.h" +#include "dns.h" + +ModuleHeader MOD_HEADER + = { + "webserver", + "1.0.0", + "Webserver", + "UnrealIRCd Team", + "unrealircd-6", + }; + +#if CHAR_MIN < 0 + #error "In UnrealIRCd char should always be unsigned. Check your compiler" +#endif + +/* How many seconds to wait with closing after sending the response */ +#define WEB_CLOSE_TIME 1 + +/* The "Server: xyz" in the response */ +#define WEB_SOFTWARE "UnrealIRCd" + +/* Macros */ +#define WEB(client) ((WebRequest *)moddata_client(client, webserver_md).ptr) +#define WEBSERVER(client) ((client->local && client->local->listener) ? client->local->listener->webserver : NULL) +#define reset_handshake_timeout(client, delta) do { client->local->creationtime = TStime() - iConf.handshake_timeout + delta; } while(0) + +/* Forward declarations */ +int webserver_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length); +int webserver_packet_in(Client *client, const char *readbuf, int *length); +void webserver_mdata_free(ModData *m); +int webserver_handle_packet(Client *client, const char *readbuf, int length); +int webserver_handle_handshake(Client *client, const char *readbuf, int *length); +int webserver_handle_request_header(Client *client, const char *readbuf, int *length); +void _webserver_send_response(Client *client, int status, char *msg); +void _webserver_close_client(Client *client); + +/* Global variables */ +ModDataInfo *webserver_md; + +MOD_TEST() +{ + EfunctionAddVoid(modinfo->handle, EFUNC_WEBSERVER_SEND_RESPONSE, _webserver_send_response); + EfunctionAddVoid(modinfo->handle, EFUNC_WEBSERVER_CLOSE_CLIENT, _webserver_close_client); + return MOD_SUCCESS; +} + +MOD_INIT() +{ + ModDataInfo mreq; + + MARK_AS_OFFICIAL_MODULE(modinfo); + + //HookAdd(modinfo->handle, HOOKTYPE_PACKET, INT_MAX, webserver_packet_out); + HookAdd(modinfo->handle, HOOKTYPE_RAWPACKET_IN, INT_MIN, webserver_packet_in); + + memset(&mreq, 0, sizeof(mreq)); + mreq.name = "web"; + mreq.serialize = NULL; + mreq.unserialize = NULL; + mreq.free = webserver_mdata_free; + mreq.sync = 0; + mreq.type = MODDATATYPE_CLIENT; + webserver_md = ModDataAdd(modinfo->handle, mreq); + + return MOD_SUCCESS; +} + +MOD_LOAD() +{ + return MOD_SUCCESS; +} + +MOD_UNLOAD() +{ + return MOD_SUCCESS; +} + +/** UnrealIRCd internals: free WebRequest object. */ +void webserver_mdata_free(ModData *m) +{ + WebRequest *wsu = (WebRequest *)m->ptr; + if (wsu) + { + safe_free(wsu->uri); + safe_free(wsu->lefttoparse); + free_nvplist(wsu->headers); + safe_free(m->ptr); + } +} + +/** Outgoing packet hook. + * Do we need this? + */ +int webserver_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length) +{ + static char utf8buf[510]; + + if (MyConnect(to) && WEB(to)) + { + // TODO: Inhibit all? + // Websocket can override though? + return 0; + } + return 0; +} + +void webserver_possible_request(Client *client, const char *buf, int len) +{ + if (len < 8) + return; + + /* Probably redundant, but just to be sure, if already tagged, then don't change it! */ + if (WEB(client)) + return; + + if (!strncmp(buf, "HEAD ", 5)) + { + moddata_client(client, webserver_md).ptr = safe_alloc(sizeof(WebRequest)); + WEB(client)->method = HTTP_METHOD_HEAD; + } else + if (!strncmp(buf, "GET ", 4)) + { + moddata_client(client, webserver_md).ptr = safe_alloc(sizeof(WebRequest)); + WEB(client)->method = HTTP_METHOD_GET; + } else + if (!strncmp(buf, "PUT ", 4)) + { + moddata_client(client, webserver_md).ptr = safe_alloc(sizeof(WebRequest)); + WEB(client)->method = HTTP_METHOD_PUT; + } else + if (!strncmp(buf, "POST ", 5)) + { + moddata_client(client, webserver_md).ptr = safe_alloc(sizeof(WebRequest)); + WEB(client)->method = HTTP_METHOD_POST; + } +} + +/** Incoming packet hook. This processes web requests. + * NOTE The different return values: + * -1 means: don't touch this client anymore, it has or might have been killed! + * 0 means: don't process this data, but you can read another packet if you want + * >0 means: process this data (regular IRC data, non-web stuff) + */ +int webserver_packet_in(Client *client, const char *readbuf, int *length) +{ + if ((client->local->traffic.messages_received == 0) && WEBSERVER(client)) + webserver_possible_request(client, readbuf, *length); + + if (!WEB(client)) + return 1; /* "normal" IRC client */ + + if (WEB(client)->request_header_parsed) + return WEBSERVER(client)->handle_data(client, WEB(client), readbuf, *length); + + /* else.. */ + return webserver_handle_request_header(client, readbuf, length); +} + +/** Helper function to parse the HTTP header consisting of multiple 'Key: value' pairs */ +int webserver_handshake_helper(char *buffer, int len, char **key, char **value, char **lastloc, int *end_of_request) +{ + static char buf[4096], *nextptr; + char *p; + char *k = NULL, *v = NULL; + int foundlf = 0; + + if (buffer) + { + /* Initialize */ + if (len > sizeof(buf) - 1) + len = sizeof(buf) - 1; + + memcpy(buf, buffer, len); + buf[len] = '\0'; + nextptr = buf; + } + + *end_of_request = 0; + + p = nextptr; + + if (!p) + { + *key = *value = NULL; + return 0; /* done processing data */ + } + + if (!strncmp(p, "\n", 1) || !strncmp(p, "\r\n", 2)) + { + *key = *value = NULL; + *end_of_request = 1; + return 0; + } + + /* Note: p *could* point to the NUL byte ('\0') */ + + /* Special handling for GET line itself. */ + if (!strncmp(p, "HEAD ", 5) ||!strncmp(p, "GET ", 4) || !strncmp(p, "PUT ", 4) || !strncmp(p, "POST ", 5)) + { + k = "REQUEST"; + p = strchr(p, ' ') + 1; /* space (0x20) is guaranteed to be there, see strncmp above */ + v = p; /* SET VALUE */ + nextptr = NULL; /* set to "we are done" in case next for loop fails */ + for (; *p; p++) + { + if (*p == ' ') + { + *p = '\0'; /* terminate before "HTTP/1.X" part */ + } + else if (*p == '\r') + { + *p = '\0'; /* eat silently, but don't consider EOL */ + } + else if (*p == '\n') + { + *p = '\0'; + nextptr = p+1; /* safe, there is data or at least a \0 there */ + break; + } + } + *key = k; + *value = v; + return 1; + } + + /* Header parsing starts here. + * Example line "Host: www.unrealircd.org" + */ + k = p; /* SET KEY */ + + /* First check if the line contains a terminating \n. If not, don't process it + * as it may have been a cut header. + */ + for (; *p; p++) + { + if (*p == '\n') + { + foundlf = 1; + break; + } + } + + if (!foundlf) + { + *key = *value = NULL; + *lastloc = k; + return 0; + } + + p = k; + + for (; *p; p++) + { + if ((*p == '\n') || (*p == '\r')) + { + /* Reached EOL but 'value' not found */ + *p = '\0'; + break; + } + if (*p == ':') + { + *p++ = '\0'; + if (*p++ != ' ') + break; /* missing mandatory space after ':' */ + + v = p; /* SET VALUE */ + nextptr = NULL; /* set to "we are done" in case next for loop fails */ + for (; *p; p++) + { + if (*p == '\r') + { + *p = '\0'; /* eat silently, but don't consider EOL */ + } + else if (*p == '\n') + { + *p = '\0'; + nextptr = p+1; /* safe, there is data or at least a \0 there */ + break; + } + } + /* A key-value pair was succesfully parsed, return it */ + *key = k; + *value = v; + return 1; + } + } + + /* Fatal parse error */ + *key = *value = NULL; + return 0; +} + +/** Check if there is any data at the end of the request */ +char *find_end_of_request(char *header, int totalsize, int *remaining_bytes) +{ + char *nextframe1; + char *nextframe2; + char *nextframe = NULL; + + // find first occurance, yeah this is just stupid, but it works. + nextframe1 = strstr(header, "\r\n\r\n"); // = +4 + nextframe2 = strstr(header, "\n\n"); // = +2 + if (nextframe1 && nextframe2) + { + if (nextframe1 < nextframe2) + { + nextframe = nextframe1 + 4; + } else { + nextframe = nextframe2 + 2; + } + } else + if (nextframe1) + { + nextframe = nextframe1 + 4; + } else + if (nextframe2) + { + nextframe = nextframe2 + 2; + } + if (nextframe) + { + *remaining_bytes = totalsize - (nextframe - header); + if (*remaining_bytes > 0) + return nextframe; + } + return NULL; +} + +/** Handle HTTP request + * Yes, I'm going to assume that the header fits in one packet and one packet only. + */ +int webserver_handle_request_header(Client *client, const char *readbuf, int *length) +{ + char *key, *value; + int r, end_of_request; + static char netbuf[16384]; + static char netbuf2[16384]; + char *lastloc = NULL; + int n, maxcopy, nprefix=0; + int totalsize; + + /* Totally paranoid: */ + memset(netbuf, 0, sizeof(netbuf)); + memset(netbuf2, 0, sizeof(netbuf2)); + + /** Frame re-assembling starts here **/ + if (WEB(client)->lefttoparse) + { + strlcpy(netbuf, WEB(client)->lefttoparse, sizeof(netbuf)); + nprefix = strlen(netbuf); + } + maxcopy = sizeof(netbuf) - nprefix - 1; + /* (Need to do some manual checking here as strlen() can't be safely used + * on readbuf. Same is true for strlncat since it uses strlen().) + */ + n = *length; + if (n > maxcopy) + n = maxcopy; + if (n <= 0) + { + webserver_close_client(client); // Oversized line + return -1; + } + memcpy(netbuf+nprefix, readbuf, n); /* SAFE: see checking above */ + totalsize = n + nprefix; + netbuf[totalsize] = '\0'; + memcpy(netbuf2, netbuf, totalsize+1); // copy, including the "always present \0 at the end just in case we use strstr etc". + safe_free(WEB(client)->lefttoparse); + + /** Now step through the lines.. **/ + for (r = webserver_handshake_helper(netbuf, strlen(netbuf), &key, &value, &lastloc, &end_of_request); + r; + r = webserver_handshake_helper(NULL, 0, &key, &value, &lastloc, &end_of_request)) + { + if (!strcasecmp(key, "REQUEST")) + { + // TODO: guard for a 16k URI ? ;D + safe_strdup(WEB(client)->uri, value); + } else + { + add_nvplist(&WEB(client)->headers, WEB(client)->num_headers, key, value); + } + } + + if (end_of_request) + { + int n; + int remaining_bytes = 0; + char *nextframe; + + WEB(client)->request_header_parsed = 1; + n = WEBSERVER(client)->handle_request(client, WEB(client)); + if ((n <= 0) || IsDead(client)) + return n; /* byebye */ + + /* There could be data directly after the request header (eg for a PUT), + * check for it here so it isn't lost. + */ + nextframe = find_end_of_request(netbuf2, totalsize, &remaining_bytes); + if (nextframe) + return WEBSERVER(client)->handle_data(client, WEB(client), nextframe, remaining_bytes); + return 0; + } + + if (lastloc) + { + /* Last line was cut somewhere, save it for next round. */ + safe_strdup(WEB(client)->lefttoparse, lastloc); + } + return 0; /* don't let UnrealIRCd process this */ +} + +void _webserver_send_response(Client *client, int status, char *msg) +{ + char buf[512]; + char *statusmsg = "???"; + + if (status == 200) + statusmsg = "OK"; + else if (status == 201) + statusmsg = "Created"; + else if (status == 400) + statusmsg = "Bad Request"; + else if (status == 403) + statusmsg = "Forbidden"; + else if (status == 404) + statusmsg = "Not Found"; + else if (status == 416) + statusmsg = "Range Not Satisfiable"; + + snprintf(buf, sizeof(buf), + "HTTP/1.1 %d %s\r\nServer: %s\r\nConnection: close\r\n\r\n%s\n", + status, statusmsg, WEB_SOFTWARE, msg); + + dbuf_put(&client->local->sendQ, buf, strlen(buf)); + send_queued(client); + webserver_close_client(client); +} + +/** Close a web client softly, after data has been sent. */ +void _webserver_close_client(Client *client) +{ + if (DBufLength(&client->local->sendQ) == 0) + { + dead_socket(client, ""); + } else { + send_queued(client); + reset_handshake_timeout(client, WEB_CLOSE_TIME); + } +} diff --git a/src/modules/websocket.c b/src/modules/websocket.c index fd9148e54..71927d507 100644 --- a/src/modules/websocket.c +++ b/src/modules/websocket.c @@ -74,7 +74,6 @@ struct HTTPForwardedHeader int websocket_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); int websocket_config_run_ex(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr); int websocket_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length); -int websocket_packet_in(Client *client, const char *readbuf, int *length); void websocket_mdata_free(ModData *m); int websocket_handle_packet(Client *client, const char *readbuf, int length); int websocket_handle_handshake(Client *client, const char *readbuf, int *length); @@ -86,6 +85,8 @@ int websocket_send_pong(Client *client, const char *buf, int len); int websocket_secure_connect(Client *client); struct HTTPForwardedHeader *websocket_parse_forwarded_header(char *input); int websocket_ip_compare(const char *ip1, const char *ip2); +int websocket_handle_websocket(Client *client, WebRequest *web, const char *readbuf2, int length2); +int websocket_handle_request(Client *client, WebRequest *web); /* Global variables */ ModDataInfo *websocket_md; @@ -105,7 +106,6 @@ MOD_INIT() HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN_EX, 0, websocket_config_run_ex); HookAdd(modinfo->handle, HOOKTYPE_PACKET, INT_MAX, websocket_packet_out); - HookAdd(modinfo->handle, HOOKTYPE_RAWPACKET_IN, INT_MIN, websocket_packet_in); HookAdd(modinfo->handle, HOOKTYPE_SECURE_CONNECT, 0, websocket_secure_connect); memset(&mreq, 0, sizeof(mreq)); @@ -224,6 +224,9 @@ int websocket_config_run_ex(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr return 0; l = (ConfigItem_listen *)ptr; + l->webserver = safe_alloc(sizeof(WebServer)); + l->webserver->handle_request = websocket_handle_request; + l->webserver->handle_data = websocket_handle_websocket; for (cep = ce->items; cep; cep = cep->next) { @@ -291,7 +294,7 @@ int websocket_packet_out(Client *from, Client *to, Client *intended_to, char **m return 0; } -int websocket_handle_websocket(Client *client, const char *readbuf2, int length2) +int websocket_handle_websocket(Client *client, WebRequest *web, const char *readbuf2, int length2) { int n; char *ptr; @@ -336,170 +339,6 @@ int websocket_handle_websocket(Client *client, const char *readbuf2, int length2 return 0; } -/** Incoming packet hook. - * This processes websocket frames, if this is a websocket connection. - * NOTE The different return values: - * -1 means: don't touch this client anymore, it has or might have been killed! - * 0 means: don't process this data, but you can read another packet if you want - * >0 means: process this data (regular IRC data, non-websocket stuff) - */ -int websocket_packet_in(Client *client, const char *readbuf, int *length) -{ - if ((client->local->traffic.messages_received == 0) && - WEBSOCKET_PORT(client) && - !WSU(client) && - (*length > 8) && - !strncmp(readbuf, "GET ", 4)) - { - /* Allocate a new WebSocketUser struct for this session */ - moddata_client(client, websocket_md).ptr = safe_alloc(sizeof(WebSocketUser)); - WSU(client)->get = 1; - WSU(client)->type = client->local->listener->websocket_options; /* the default, unless the client chooses otherwise */ - } - - if (!WSU(client)) - return 1; /* "normal" IRC client */ - - if (WSU(client)->handshake_completed) - return websocket_handle_websocket(client, readbuf, *length); - /* else.. */ - return websocket_handle_handshake(client, readbuf, length); -} - -/** Helper function to parse the HTTP header consisting of multiple 'Key: value' pairs */ -int websocket_handshake_helper(char *buffer, int len, char **key, char **value, char **lastloc, int *end_of_request) -{ - static char buf[4096], *nextptr; - char *p; - char *k = NULL, *v = NULL; - int foundlf = 0; - - if (buffer) - { - /* Initialize */ - if (len > sizeof(buf) - 1) - len = sizeof(buf) - 1; - - memcpy(buf, buffer, len); - buf[len] = '\0'; - nextptr = buf; - } - - *end_of_request = 0; - - p = nextptr; - - if (!p) - { - *key = *value = NULL; - return 0; /* done processing data */ - } - - if (!strncmp(p, "\n", 1) || !strncmp(p, "\r\n", 2)) - { - *key = *value = NULL; - *end_of_request = 1; - return 0; - } - - /* Note: p *could* point to the NUL byte ('\0') */ - - /* Special handling for GET line itself. */ - if (!strncmp(p, "GET ", 4)) - { - k = "GET"; - p += 4; - v = p; /* SET VALUE */ - nextptr = NULL; /* set to "we are done" in case next for loop fails */ - for (; *p; p++) - { - if (*p == ' ') - { - *p = '\0'; /* terminate before "HTTP/1.X" part */ - } - else if (*p == '\r') - { - *p = '\0'; /* eat silently, but don't consider EOL */ - } - else if (*p == '\n') - { - *p = '\0'; - nextptr = p+1; /* safe, there is data or at least a \0 there */ - break; - } - } - *key = k; - *value = v; - return 1; - } - - /* Header parsing starts here. - * Example line "Host: www.unrealircd.org" - */ - k = p; /* SET KEY */ - - /* First check if the line contains a terminating \n. If not, don't process it - * as it may have been a cut header. - */ - for (; *p; p++) - { - if (*p == '\n') - { - foundlf = 1; - break; - } - } - - if (!foundlf) - { - *key = *value = NULL; - *lastloc = k; - return 0; - } - - p = k; - - for (; *p; p++) - { - if ((*p == '\n') || (*p == '\r')) - { - /* Reached EOL but 'value' not found */ - *p = '\0'; - break; - } - if (*p == ':') - { - *p++ = '\0'; - if (*p++ != ' ') - break; /* missing mandatory space after ':' */ - - v = p; /* SET VALUE */ - nextptr = NULL; /* set to "we are done" in case next for loop fails */ - for (; *p; p++) - { - if (*p == '\r') - { - *p = '\0'; /* eat silently, but don't consider EOL */ - } - else if (*p == '\n') - { - *p = '\0'; - nextptr = p+1; /* safe, there is data or at least a \0 there */ - break; - } - } - /* A key-value pair was succesfully parsed, return it */ - *key = k; - *value = v; - return 1; - } - } - - /* Fatal parse error */ - *key = *value = NULL; - return 0; -} - #define FHEADER_STATE_NAME 0 #define FHEADER_STATE_VALUE 1 #define FHEADER_STATE_VALUE_QUOTED 2 @@ -635,9 +474,49 @@ struct HTTPForwardedHeader *websocket_parse_forwarded_header(char *input) return &forwarded; } -/** Finally, validate the websocket request (handshake) and proceed or reject. */ -int websocket_handshake_valid(Client *client) +/** We got a HTTP(S) request and we need to check if we can upgrade the connection + * to a websocket connection. + */ +int websocket_handle_request(Client *client, WebRequest *web) { + NameValuePrioList *r; + const char *key, *value; + + /* Allocate a new WebSocketUser struct for this session */ + moddata_client(client, websocket_md).ptr = safe_alloc(sizeof(WebSocketUser)); + /* ...and set the default protocol (text or binary) */ + WSU(client)->type = client->local->listener->websocket_options; + + /** Now step through the lines.. **/ + for (r = web->headers; r; r = r->next) + { + key = r->name; + value = r->value; + if (!strcasecmp(key, "Sec-WebSocket-Key")) + { + if (strchr(value, ':')) + { + /* This would cause unserialization issues. Should be base64 anyway */ + webserver_send_response(client, 400, "Invalid characters in Sec-WebSocket-Key"); + return -1; + } + safe_strdup(WSU(client)->handshake_key, value); + } else + if (!strcasecmp(key, "Sec-WebSocket-Protocol")) + { + /* Save it here, will be processed later */ + safe_strdup(WSU(client)->sec_websocket_protocol, value); + } else + if (!strcasecmp(key, "Forwarded")) + { + /* will be processed later too */ + safe_strdup(WSU(client)->forwarded, value); + } + } + + /** Finally, validate the websocket request (handshake) and proceed or reject. */ + + /* Not websocket and webredir loaded? Let that module serve a redirect. */ if (!WSU(client)->handshake_key) { if (is_module_loaded("webredir")) @@ -645,9 +524,11 @@ int websocket_handshake_valid(Client *client) const char *parx[2] = { NULL, NULL }; do_cmd(client, NULL, "GET", 1, parx); } - dead_socket(client, "Invalid WebSocket request"); + webserver_send_response(client, 404, "This port is for IRC WebSocket only"); return 0; } + + /* Sec-WebSocket-Protocol (optional) */ if (WSU(client)->sec_websocket_protocol) { char *p = NULL, *name; @@ -684,6 +565,8 @@ int websocket_handshake_valid(Client *client) safe_free(WSU(client)->sec_websocket_protocol); } } + + /* Check forwarded header (by k4be) */ if (WSU(client)->forwarded) { struct HTTPForwardedHeader *forwarded; @@ -693,7 +576,7 @@ int websocket_handshake_valid(Client *client) if (BadPtr(client->local->listener->websocket_forward) || !websocket_ip_compare(client->local->listener->websocket_forward, client->ip)) { unreal_log(ULOG_WARNING, "websocket", "UNAUTHORIZED_FORWARDED_HEADER", client, "Received unauthorized Forwarded header from $ip", log_data_string("ip", client->ip)); - dead_socket(client, "Forwarded: no access"); + webserver_send_response(client, 403, "Forwarded: no access"); return 0; } /* parse the header */ @@ -702,7 +585,7 @@ int websocket_handshake_valid(Client *client) if (!is_valid_ip(forwarded->ip)) { unreal_log(ULOG_WARNING, "websocket", "INVALID_FORWARDED_IP", client, "Received invalid IP in Forwarded header from $ip", log_data_string("ip", client->ip)); - dead_socket(client, "Forwarded: invalid IP"); + webserver_send_response(client, 400, "Forwarded: invalid IP"); return 0; } /* store data */ @@ -738,6 +621,8 @@ int websocket_handshake_valid(Client *client) } RunHook(HOOKTYPE_IP_CHANGE, client, oldip); } + + websocket_handshake_send_response(client); return 1; } @@ -752,83 +637,6 @@ int websocket_secure_connect(Client *client) return 0; } -/** Handle client GET WebSocket handshake. - * Yes, I'm going to assume that the header fits in one packet and one packet only. - */ -int websocket_handle_handshake(Client *client, const char *readbuf, int *length) -{ - char *key, *value; - int r, end_of_request; - char netbuf[2048]; - char *lastloc = NULL; - int n, maxcopy, nprefix=0; - - /** Frame re-assembling starts here **/ - *netbuf = '\0'; - if (WSU(client)->lefttoparse) - { - strlcpy(netbuf, WSU(client)->lefttoparse, sizeof(netbuf)); - nprefix = strlen(netbuf); - } - maxcopy = sizeof(netbuf) - nprefix - 1; - /* (Need to some manual checking here as strlen() can't be safely used - * on readbuf. Same is true for strlncat since it uses strlen().) - */ - n = *length; - if (n > maxcopy) - n = maxcopy; - if (n <= 0) - { - dead_socket(client, "Oversized line"); - return -1; - } - memcpy(netbuf+nprefix, readbuf, n); /* SAFE: see checking above */ - netbuf[n+nprefix] = '\0'; - safe_free(WSU(client)->lefttoparse); - - /** Now step through the lines.. **/ - for (r = websocket_handshake_helper(netbuf, strlen(netbuf), &key, &value, &lastloc, &end_of_request); - r; - r = websocket_handshake_helper(NULL, 0, &key, &value, &lastloc, &end_of_request)) - { - if (!strcasecmp(key, "Sec-WebSocket-Key")) - { - if (strchr(value, ':')) - { - /* This would cause unserialization issues. Should be base64 anyway */ - dead_socket(client, "Invalid characters in Sec-WebSocket-Key"); - return -1; - } - safe_strdup(WSU(client)->handshake_key, value); - } else - if (!strcasecmp(key, "Sec-WebSocket-Protocol")) - { - /* Save it here, will be processed later */ - safe_strdup(WSU(client)->sec_websocket_protocol, value); - } else - if (!strcasecmp(key, "Forwarded")) - { - /* will be processed later too */ - safe_strdup(WSU(client)->forwarded, value); - } - } - - if (end_of_request) - { - if (!websocket_handshake_valid(client) || IsDead(client)) - return -1; - websocket_handshake_send_response(client); - return 0; - } - - if (lastloc) - { - /* Last line was cut somewhere, save it for next round. */ - safe_strdup(WSU(client)->lefttoparse, lastloc); - } - return 0; /* don't let UnrealIRCd process this */ -} - /** Complete the handshake by sending the appropriate HTTP 101 response etc. */ int websocket_handshake_send_response(Client *client) {