unrealircd/src/modules/webserver.c

676 lines
17 KiB
C

/*
* 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);
int _webserver_handle_body(Client *client, WebRequest *web, const char *readbuf, int length);
/* Global variables */
ModDataInfo *webserver_md;
MOD_TEST()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
EfunctionAddVoid(modinfo->handle, EFUNC_WEBSERVER_SEND_RESPONSE, _webserver_send_response);
EfunctionAddVoid(modinfo->handle, EFUNC_WEBSERVER_CLOSE_CLIENT, _webserver_close_client);
EfunctionAdd(modinfo->handle, EFUNC_WEBSERVER_HANDLE_BODY, _webserver_handle_body);
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);
free_nvplist(wsu->headers);
safe_free(wsu->lefttoparse);
safe_free(wsu->request_buffer);
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;
}
HttpMethod webserver_get_method(const char *buf)
{
if (!strncmp(buf, "HEAD ", 5))
return HTTP_METHOD_HEAD;
if (!strncmp(buf, "GET ", 4))
return HTTP_METHOD_GET;
if (!strncmp(buf, "PUT ", 4))
return HTTP_METHOD_PUT;
if (!strncmp(buf, "POST ", 5))
return HTTP_METHOD_POST;
return HTTP_METHOD_NONE; /* invalid */
}
void webserver_possible_request(Client *client, const char *buf, int len)
{
HttpMethod method;
if (len < 8)
return;
/* Probably redundant, but just to be sure, if already tagged, then don't change it! */
if (WEB(client))
return;
method = webserver_get_method(buf);
if (method == HTTP_METHOD_NONE)
return; /* invalid */
moddata_client(client, webserver_md).ptr = safe_alloc(sizeof(WebRequest));
WEB(client)->method = method;
/* Set some default values: */
WEB(client)->content_length = -1;
WEB(client)->config_max_request_buffer_size = 4096; /* 4k */
}
/** 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_body(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 (webserver_get_method(p) != HTTP_METHOD_NONE)
{
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 (BadPtr(value))
continue; /* skip empty values */
if (!strcasecmp(key, "REQUEST"))
{
safe_strdup(WEB(client)->uri, value);
} else
{
if (!strcasecmp(key, "Content-Length"))
{
WEB(client)->content_length = atoll(value);
} else
if (!strcasecmp(key, "Transfer-Encoding"))
{
if (!strcasecmp(value, "chunked"))
WEB(client)->transfer_encoding = TRANSFER_ENCODING_CHUNKED;
}
add_nvplist(&WEB(client)->headers, WEB(client)->num_headers, key, value);
}
}
if (end_of_request)
{
int n;
int remaining_bytes = 0;
char *nextframe;
/* Some sanity checks */
if (!WEB(client)->uri)
{
webserver_send_response(client, 400, "Malformed HTTP request");
return -1;
}
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 POST or 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_body(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 */
}
/** Send a HTTP(S) response.
* @param client Client to send to
* @param status HTTP status code
* @param msg The message body.
* @note if 'msgs' is NULL then don't close the connection.
*/
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 == 500)
statusmsg = "Internal Server Error";
else if (status == 400)
statusmsg = "Bad Request";
else if (status == 401)
statusmsg = "Unauthorized";
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",
status, statusmsg, WEB_SOFTWARE);
if (msg)
{
strlcat(buf, msg, sizeof(buf));
strlcat(buf, "\n", sizeof(buf));
}
dbuf_put(&client->local->sendQ, buf, strlen(buf));
if (msg)
webserver_close_client(client);
}
/** Close a web client softly, after data has been sent. */
void _webserver_close_client(Client *client)
{
send_queued(client);
if (DBufLength(&client->local->sendQ) == 0)
{
exit_client(client, NULL, "End of request");
//dead_socket(client, "");
} else {
send_queued(client);
reset_handshake_timeout(client, WEB_CLOSE_TIME);
}
}
int webserver_handle_body_append_buffer(Client *client, const char *buf, int len)
{
/* Guard.. */
if (len <= 0)
{
dead_socket(client, "HTTP request error");
return 0;
}
if (WEB(client)->request_buffer)
{
long long newsize = WEB(client)->request_buffer_size + len + 1;
if (newsize > WEB(client)->config_max_request_buffer_size)
{
/* We would overflow */
unreal_log(ULOG_WARNING, "webserver", "HTTP_BODY_TOO_LARGE", client,
"[webserver] Client $client: request body too large ($length)",
log_data_integer("length", newsize));
dead_socket(client, "");
return 0;
}
WEB(client)->request_buffer = realloc(WEB(client)->request_buffer, newsize);
} else
{
if (len + 1 > WEB(client)->config_max_request_buffer_size)
{
/* We would overflow */
unreal_log(ULOG_WARNING, "webserver", "HTTP_BODY_TOO_LARGE", client,
"[webserver] Client $client: request body too large ($length)",
log_data_integer("length", len+1));
dead_socket(client, "");
return 0;
}
WEB(client)->request_buffer = malloc(len+1);
}
memcpy(WEB(client)->request_buffer + WEB(client)->request_buffer_size, buf, len);
WEB(client)->request_buffer_size += len;
WEB(client)->request_buffer[WEB(client)->request_buffer_size] = '\0';
return 1;
}
/** Handle HTTP body parsing, eg for a PUT request, concatting it all together.
* @param client The client
* @param web The WEB(client)
* @param readbuf Packet in the read buffer
* @param pktsize Packet size of the read buffer
* @return 1 to continue processing, 0 if client is killed.
*/
int _webserver_handle_body(Client *client, WebRequest *web, const char *readbuf, int pktsize)
{
char *buf;
long long n;
char *free_this_buffer = NULL;
if (WEB(client)->transfer_encoding == TRANSFER_ENCODING_NONE)
{
if (!webserver_handle_body_append_buffer(client, readbuf, pktsize))
return 0;
if ((WEB(client)->content_length >= 0) &&
(WEB(client)->request_buffer_size >= WEB(client)->content_length))
{
WEB(client)->request_body_complete = 1;
}
return 1;
}
/* Fill 'buf' nd set 'buflen' with what we had + what we have now.
* Makes things easy.
*/
if (WEB(client)->lefttoparse)
{
n = WEB(client)->lefttoparselen + pktsize;
free_this_buffer = buf = safe_alloc(n);
memcpy(buf, WEB(client)->lefttoparse, WEB(client)->lefttoparselen);
memcpy(buf+WEB(client)->lefttoparselen, readbuf, pktsize);
safe_free(WEB(client)->lefttoparse);
WEB(client)->lefttoparselen = 0;
} else {
n = pktsize;
free_this_buffer = buf = safe_alloc(n);
memcpy(buf, readbuf, n);
}
/* Chunked transfers.. yayyyy.. */
while (n > 0)
{
if (WEB(client)->chunk_remaining > 0)
{
/* Eat it */
int eat = MIN(WEB(client)->chunk_remaining, n);
if (!webserver_handle_body_append_buffer(client, buf, eat))
{
/* fatal error such as size exceeded */
safe_free(free_this_buffer);
return 0;
}
n -= eat;
buf += eat;
WEB(client)->chunk_remaining -= eat;
} else
{
int gotlf = 0;
int i;
/* First check if it is a (trailing) empty line,
* eg from a previous chunk. Skip over.
*/
if ((n >= 2) && !strncmp(buf, "\r\n", 2))
{
buf += 2;
n -= 2;
} else
if ((n >= 1) && !strncmp(buf, "\n", 1))
{
buf++;
n--;
}
/* Now we are (possibly) at the chunk size line,
* this is or example '7f' + newline.
* So first, check if we have a newline at all.
*/
for (i=0; i < n; i++)
{
if (buf[i] == '\n')
{
gotlf = 1;
break;
}
}
if (!gotlf)
{
/* The line telling us the chunk size is incomplete,
* as it does not contain an \n. Wait for more data
* from the network socket.
*/
if (n > 0)
{
/* Store what we have first.. */
WEB(client)->lefttoparselen = n;
WEB(client)->lefttoparse = safe_alloc(n);
memcpy(WEB(client)->lefttoparse, buf, n);
}
safe_free(free_this_buffer);
return 1; /* WE WANT MORE! */
}
buf[i] = '\0'; /* cut at LF */
i++; /* point to next data */
WEB(client)->chunk_remaining = strtol(buf, NULL, 16);
if (WEB(client)->chunk_remaining < 0)
{
unreal_log(ULOG_WARNING, "webserver", "WEB_NEGATIVE_CHUNK", client,
"Webrequest from $client: Negative chunk encountered");
safe_free(free_this_buffer);
dead_socket(client, "");
return 0;
}
if (WEB(client)->chunk_remaining == 0)
{
/* DONE! */
WEB(client)->request_body_complete = 1;
safe_free(free_this_buffer);
return 1;
}
buf += i;
n -= i;
}
}
safe_free(free_this_buffer);
return 1;
}