3715 lines
95 KiB
C
3715 lines
95 KiB
C
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// This is free and unencumbered software released into the public domain. ///
|
|
/// ///
|
|
/// Anyone is free to copy, modify, publish, use, compile, sell, or ///
|
|
/// distribute this software, either in source code form or as a compiled ///
|
|
/// binary, for any purpose, commercial or non-commercial, and by any ///
|
|
/// means. ///
|
|
/// ///
|
|
/// In jurisdictions that recognize copyright laws, the author or authors ///
|
|
/// of this software dedicate any and all copyright interest in the ///
|
|
/// software to the public domain. We make this dedication for the benefit ///
|
|
/// of the public at large and to the detriment of our heirs and ///
|
|
/// successors. We intend this dedication to be an overt act of ///
|
|
/// relinquishment in perpetuity of all present and future rights to this ///
|
|
/// software under copyright law. ///
|
|
/// ///
|
|
/// 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 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. ///
|
|
/// ///
|
|
/// For more information, please refer to <https://unlicense.org> ///
|
|
/// ///
|
|
/// NOTE: Some code was adapted from BearSSL. That code uses the MIT license. ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// HEADERS ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TODO: Clean up these headers
|
|
#include <time.h>
|
|
#include <poll.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <assert.h>
|
|
#include <stdarg.h>
|
|
#include <limits.h>
|
|
#include <signal.h> // sig_atomic_t
|
|
#include <dirent.h>
|
|
#include <stdbool.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/resource.h>
|
|
#include <arpa/inet.h>
|
|
#include "log.h"
|
|
#include "http.h"
|
|
#include "tinytemplate.h"
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// CONFIGURATION ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#ifndef HTTPS
|
|
#define HTTPS 0
|
|
#endif
|
|
|
|
#define INPUT_BUFFER_LIMIT_MB 1
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// OPTIONAL HEADERS ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#if HTTPS
|
|
#include <bearssl.h>
|
|
#endif
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// TYPES & DEFINITIONS ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#if HTTPS
|
|
typedef struct {
|
|
int type; // BR_KEYTYPE_RSA or BR_KEYTYPE_EC
|
|
union {
|
|
br_rsa_private_key rsa;
|
|
br_ec_private_key ec;
|
|
};
|
|
} PrivateKey;
|
|
|
|
typedef struct {
|
|
br_x509_certificate *items;
|
|
int count;
|
|
int capacity;
|
|
} CertArray;
|
|
|
|
typedef struct {
|
|
int code;
|
|
string name;
|
|
string comment;
|
|
} BearSSLErrorInfo;
|
|
#endif
|
|
|
|
typedef struct {
|
|
char *data;
|
|
size_t head;
|
|
size_t size;
|
|
size_t capacity;
|
|
} ByteQueue;
|
|
|
|
struct Connection {
|
|
int fd;
|
|
ByteQueue input;
|
|
ByteQueue output;
|
|
uint32_t ipaddr;
|
|
int served_count;
|
|
bool https;
|
|
bool closing;
|
|
bool keep_alive;
|
|
uint64_t creation_time;
|
|
uint64_t start_time;
|
|
#if HTTPS
|
|
br_ssl_server_context https_context;
|
|
char https_buffer[BR_SSL_BUFSIZE_BIDI];
|
|
#endif
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// FORWARD DECLARATIONS ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool url_parse2(string str, size_t *i, url_t *url);
|
|
static bool url_parse(string str, url_t *url);
|
|
static bool url_parse_ipv6(string str, uint16_t out[8]);
|
|
static bool url_parse_ipv4(string str, uint32_t *out);
|
|
|
|
static void byte_queue_init(ByteQueue *q);
|
|
static void byte_queue_free(ByteQueue *q);
|
|
static size_t byte_queue_size(ByteQueue *q);
|
|
static bool byte_queue_ensure_min_free_space(ByteQueue *q, size_t num);
|
|
static string byte_queue_start_write(ByteQueue *q);
|
|
static void byte_queue_end_write(ByteQueue *q, size_t num);
|
|
static string byte_queue_start_read(ByteQueue *q);
|
|
static void byte_queue_end_read(ByteQueue *q, size_t num);
|
|
static bool byte_queue_write(ByteQueue *q, string src);
|
|
static void byte_queue_patch(ByteQueue *q, size_t offset, char *src, size_t len);
|
|
|
|
#if HTTPS
|
|
static bool load_private_key_from_file(string file, PrivateKey *pkey);
|
|
static void free_private_key(PrivateKey *pkey);
|
|
static bool load_certs_from_file(string file, CertArray *array);
|
|
static void free_certs(CertArray *array);
|
|
static BearSSLErrorInfo get_bearssl_error_info(int code);
|
|
#endif
|
|
|
|
static bool set_blocking(int fd, bool blocking);
|
|
static bool read_from_socket(int fd, ByteQueue *queue);
|
|
static bool write_to_socket(int fd, ByteQueue *queue);
|
|
static int create_listening_socket(string addr, int port);
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// GLOBALS ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static volatile sig_atomic_t stop = 0;
|
|
|
|
static Connection *conns;
|
|
static int num_conns = 0;
|
|
static int max_conns = 0;
|
|
|
|
static struct pollfd *pollarray;
|
|
|
|
static uint64_t now;
|
|
static uint64_t real_now;
|
|
|
|
static int insecure_fd;
|
|
static int secure_fd;
|
|
|
|
static bool show_io;
|
|
static bool show_requests;
|
|
static bool access_log;
|
|
static int keep_alive_max_requests;
|
|
static int connection_timeout_sec;
|
|
static int closing_timeout_sec;
|
|
static int request_timeout_sec;
|
|
static int log_flush_timeout_sec;
|
|
static void (*respond_callback)(Request, ResponseBuilder*);
|
|
|
|
#if HTTPS
|
|
static PrivateKey pkey;
|
|
static CertArray certs;
|
|
#endif
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// REQUEST PARSER ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TODO: Make sure every string in request is reasonaly long
|
|
static int parse_request_head(string str, Request *request)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t cur;
|
|
if (len > 2
|
|
&& src[0] == 'G'
|
|
&& src[1] == 'E'
|
|
&& src[2] == 'T') {
|
|
request->method = M_GET;
|
|
cur = 3;
|
|
} else if (len > 3
|
|
&& src[0] == 'H'
|
|
&& src[1] == 'E'
|
|
&& src[2] == 'A'
|
|
&& src[3] == 'D') {
|
|
request->method = M_HEAD;
|
|
cur = 4;
|
|
} else if (len > 3
|
|
&& src[0] == 'P'
|
|
&& src[1] == 'O'
|
|
&& src[2] == 'S'
|
|
&& src[3] == 'T') {
|
|
request->method = M_POST;
|
|
cur = 4;
|
|
} else if (len > 2
|
|
&& src[0] == 'P'
|
|
&& src[1] == 'U'
|
|
&& src[2] == 'T') {
|
|
request->method = M_PUT;
|
|
cur = 3;
|
|
} else if (len > 5
|
|
&& src[0] == 'D'
|
|
&& src[1] == 'E'
|
|
&& src[2] == 'L'
|
|
&& src[3] == 'E'
|
|
&& src[4] == 'T'
|
|
&& src[5] == 'E') {
|
|
request->method = M_DELETE;
|
|
cur = 6;
|
|
} else if (len > 6
|
|
&& src[0] == 'C'
|
|
&& src[1] == 'O'
|
|
&& src[2] == 'N'
|
|
&& src[3] == 'N'
|
|
&& src[4] == 'E'
|
|
&& src[5] == 'C'
|
|
&& src[6] == 'T') {
|
|
request->method = M_CONNECT;
|
|
cur = 7;
|
|
} else if (len > 6
|
|
&& src[0] == 'O'
|
|
&& src[1] == 'P'
|
|
&& src[2] == 'T'
|
|
&& src[3] == 'I'
|
|
&& src[4] == 'O'
|
|
&& src[5] == 'N'
|
|
&& src[6] == 'S') {
|
|
request->method = M_OPTIONS;
|
|
cur = 7;
|
|
} else if (len > 4
|
|
&& src[0] == 'T'
|
|
&& src[1] == 'R'
|
|
&& src[2] == 'A'
|
|
&& src[3] == 'C'
|
|
&& src[4] == 'E') {
|
|
request->method = M_TRACE;
|
|
cur = 5;
|
|
} else if (len > 4
|
|
&& src[0] == 'P'
|
|
&& src[1] == 'A'
|
|
&& src[2] == 'T'
|
|
&& src[3] == 'C'
|
|
&& src[4] == 'H') {
|
|
request->method = M_PATCH;
|
|
cur = 5;
|
|
} else {
|
|
return P_BADMETHOD;
|
|
}
|
|
|
|
if (cur == len || src[cur] != ' ')
|
|
return P_INCOMPLETE;
|
|
cur++;
|
|
|
|
if (!url_parse2(str, &cur, &request->url))
|
|
return P_BADURL;
|
|
|
|
if (cur == len || src[cur] != ' ')
|
|
return P_INCOMPLETE;
|
|
cur++;
|
|
|
|
if (cur+4 >= len
|
|
|| src[cur+0] != 'H'
|
|
|| src[cur+1] != 'T'
|
|
|| src[cur+2] != 'T'
|
|
|| src[cur+3] != 'P'
|
|
|| src[cur+4] != '/'
|
|
|| !is_digit(src[cur+5]))
|
|
return P_BADVERSION;
|
|
cur += 5;
|
|
request->major = src[cur] - '0';
|
|
cur++;
|
|
|
|
if (cur < len && src[cur] == '.') {
|
|
cur++;
|
|
if (cur == len || !is_digit(src[cur]))
|
|
return P_BADVERSION;
|
|
request->minor = src[cur] - '0';
|
|
cur++;
|
|
} else {
|
|
request->minor = 0;
|
|
}
|
|
|
|
if (cur+1 >= len
|
|
|| src[cur+0] != '\r'
|
|
|| src[cur+1] != '\n')
|
|
return P_INCOMPLETE;
|
|
cur += 2;
|
|
|
|
request->nheaders = 0;
|
|
while (cur+1 >= len
|
|
|| src[cur+0] != '\r'
|
|
|| src[cur+1] != '\n') {
|
|
|
|
string name;
|
|
string value;
|
|
|
|
size_t start = cur;
|
|
|
|
// TODO: More robust
|
|
while (cur < len && src[cur] != ':')
|
|
cur++;
|
|
|
|
name.data = src + start;
|
|
name.size = cur - start;
|
|
|
|
if (cur == len)
|
|
return P_BADHEADER;
|
|
cur++; // :
|
|
|
|
// TODO: More robust
|
|
start = cur;
|
|
while (cur < len && src[cur] != '\r')
|
|
cur++;
|
|
value.data = src + start;
|
|
value.size = cur - start;
|
|
|
|
cur++; // \r
|
|
if (cur == len || src[cur] != '\n')
|
|
return P_BADHEADER;
|
|
cur++; // \n
|
|
|
|
if (request->nheaders < MAX_HEADERS) {
|
|
request->headers[request->nheaders].name = name;
|
|
request->headers[request->nheaders].value = value;
|
|
request->nheaders++;
|
|
}
|
|
}
|
|
// cur here points to the \r in \r\n
|
|
request->content = NULLSTR;
|
|
return P_OK;
|
|
}
|
|
|
|
static bool find_header(Request *request, string name, string *value)
|
|
{
|
|
for (int i = 0; i < request->nheaders; i++)
|
|
if (string_match_case_insensitive(request->headers[i].name, name)) {
|
|
*value = request->headers[i].value;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static string get_status_string(int status)
|
|
{
|
|
switch(status)
|
|
{
|
|
case 100: return LIT("Continue");
|
|
case 101: return LIT("Switching Protocols");
|
|
case 102: return LIT("Processing");
|
|
|
|
case 200: return LIT("OK");
|
|
case 201: return LIT("Created");
|
|
case 202: return LIT("Accepted");
|
|
case 203: return LIT("Non-Authoritative Information");
|
|
case 204: return LIT("No Content");
|
|
case 205: return LIT("Reset Content");
|
|
case 206: return LIT("Partial Content");
|
|
case 207: return LIT("Multi-Status");
|
|
case 208: return LIT("Already Reported");
|
|
|
|
case 300: return LIT("Multiple Choices");
|
|
case 301: return LIT("Moved Permanently");
|
|
case 302: return LIT("Found");
|
|
case 303: return LIT("See Other");
|
|
case 304: return LIT("Not Modified");
|
|
case 305: return LIT("Use Proxy");
|
|
case 306: return LIT("Switch Proxy");
|
|
case 307: return LIT("Temporary Redirect");
|
|
case 308: return LIT("Permanent Redirect");
|
|
|
|
case 400: return LIT("Bad Request");
|
|
case 401: return LIT("Unauthorized");
|
|
case 402: return LIT("Payment Required");
|
|
case 403: return LIT("Forbidden");
|
|
case 404: return LIT("Not Found");
|
|
case 405: return LIT("Method Not Allowed");
|
|
case 406: return LIT("Not Acceptable");
|
|
case 407: return LIT("Proxy Authentication Required");
|
|
case 408: return LIT("Request Timeout");
|
|
case 409: return LIT("Conflict");
|
|
case 410: return LIT("Gone");
|
|
case 411: return LIT("Length Required");
|
|
case 412: return LIT("Precondition Failed");
|
|
case 413: return LIT("Request Entity Too Large");
|
|
case 414: return LIT("Request-URI Too Long");
|
|
case 415: return LIT("Unsupported Media Type");
|
|
case 416: return LIT("Requested Range Not Satisfiable");
|
|
case 417: return LIT("Expectation Failed");
|
|
case 418: return LIT("I'm a teapot");
|
|
case 420: return LIT("Enhance your calm");
|
|
case 422: return LIT("Unprocessable Entity");
|
|
case 426: return LIT("Upgrade Required");
|
|
case 429: return LIT("Too many requests");
|
|
case 431: return LIT("Request Header Fields Too Large");
|
|
case 449: return LIT("Retry With");
|
|
case 451: return LIT("Unavailable For Legal Reasons");
|
|
|
|
case 500: return LIT("Internal Server Error");
|
|
case 501: return LIT("Not Implemented");
|
|
case 502: return LIT("Bad Gateway");
|
|
case 503: return LIT("Service Unavailable");
|
|
case 504: return LIT("Gateway Timeout");
|
|
case 505: return LIT("HTTP Version Not Supported");
|
|
case 509: return LIT("Bandwidth Limit Exceeded");
|
|
}
|
|
return LIT("???");
|
|
}
|
|
|
|
static size_t parse_content_length(string s)
|
|
{
|
|
char *src = s.data;
|
|
size_t len = s.size;
|
|
|
|
size_t cur = 0;
|
|
while (cur < len && is_space(src[cur]))
|
|
cur++;
|
|
|
|
if (cur == len || !is_digit(src[cur]))
|
|
return -1;
|
|
|
|
size_t x = 0;
|
|
do {
|
|
int d = src[cur] - '0';
|
|
if (x > (SIZE_MAX - d) / 10)
|
|
return -1;
|
|
x = x * 10 + d;
|
|
cur++;
|
|
} while (cur < len && is_digit(src[cur]));
|
|
|
|
while (cur < len && is_space(src[cur]))
|
|
cur++;
|
|
|
|
if (cur != len)
|
|
return -1;
|
|
|
|
return x;
|
|
}
|
|
|
|
static int find_and_parse_transfer_encoding(Request *request)
|
|
{
|
|
string value;
|
|
if (!find_header(request, LIT("Transfer-Encoding"), &value))
|
|
return 0;
|
|
|
|
int res = 0;
|
|
char *src = value.data;
|
|
size_t len = value.size;
|
|
size_t cur = 0;
|
|
for (;;) {
|
|
|
|
while (cur < len && (is_space(src[cur]) || src[cur] == ','))
|
|
cur++;
|
|
|
|
if (cur+6 < len
|
|
&& src[cur+0] == 'c'
|
|
&& src[cur+1] == 'h'
|
|
&& src[cur+2] == 'u'
|
|
&& src[cur+3] == 'n'
|
|
&& src[cur+4] == 'k'
|
|
&& src[cur+5] == 'e'
|
|
&& src[cur+6] == 'd') {
|
|
cur += 7;
|
|
res |= T_CHUNKED;
|
|
} else if (cur+7 < len
|
|
&& src[cur+0] == 'c'
|
|
&& src[cur+1] == 'o'
|
|
&& src[cur+2] == 'm'
|
|
&& src[cur+3] == 'p'
|
|
&& src[cur+4] == 'r'
|
|
&& src[cur+5] == 'e'
|
|
&& src[cur+6] == 's'
|
|
&& src[cur+7] == 's') {
|
|
cur += 8;
|
|
res |= T_COMPRESS;
|
|
} else if (cur+6 < len
|
|
&& src[cur+0] == 'd'
|
|
&& src[cur+1] == 'e'
|
|
&& src[cur+2] == 'f'
|
|
&& src[cur+3] == 'l'
|
|
&& src[cur+4] == 'a'
|
|
&& src[cur+5] == 't'
|
|
&& src[cur+6] == 'e') {
|
|
cur += 7;
|
|
res |= T_DEFLATE;
|
|
} else if (cur+3 < len
|
|
&& src[cur+0] == 'g'
|
|
&& src[cur+1] == 'z'
|
|
&& src[cur+2] == 'i'
|
|
&& src[cur+3] == 'p') {
|
|
cur += 4;
|
|
res |= T_GZIP;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static void response_builder_init(ResponseBuilder *b, Connection *conn)
|
|
{
|
|
b->state = R_STATUS;
|
|
b->conn = conn;
|
|
b->failed = false;
|
|
b->keep_alive = true;
|
|
b->content_length_offset = -1;
|
|
b->content_offset = -1;
|
|
}
|
|
|
|
void status_line(ResponseBuilder *b, int status)
|
|
{
|
|
if (b->state != R_STATUS)
|
|
log_fatal(LIT("Appending status line twice\n"));
|
|
if (!b->failed) {
|
|
char buf[1<<10];
|
|
string status_string = get_status_string(status);
|
|
int num = snprintf(buf, sizeof(buf), "HTTP/1.1 %d %.*s\r\n", status, (int) status_string.size, status_string.data);
|
|
assert(num > 0 && num < SIZEOF(buf));
|
|
if (!byte_queue_write(&b->conn->output, (string) {buf, num}))
|
|
b->failed = true;
|
|
}
|
|
b->state = R_HEADER;
|
|
}
|
|
|
|
void add_header(ResponseBuilder *b, string header)
|
|
{
|
|
if (b->state != R_HEADER) {
|
|
if (b->state == R_STATUS)
|
|
log_fatal(LIT("Didn't write status line before headers\n"));
|
|
else
|
|
log_fatal(LIT("Can't add headers after content\n"));
|
|
}
|
|
if (b->failed)
|
|
return;
|
|
if (!byte_queue_write(&b->conn->output, header) ||
|
|
!byte_queue_write(&b->conn->output, LIT("\r\n"))) {
|
|
b->failed = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void add_header_f(ResponseBuilder *b, const char *fmt, ...)
|
|
{
|
|
char buffer[1<<10];
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
int num = vsnprintf(buffer, sizeof(buffer), fmt, args);
|
|
va_end(args);
|
|
|
|
if (num < 0 || num >= (int) sizeof(buffer)) {
|
|
b->failed = true;
|
|
return;
|
|
}
|
|
|
|
buffer[num] = '\0';
|
|
|
|
add_header(b, (string) {buffer, num});
|
|
}
|
|
|
|
static bool should_keep_alive(Connection *conn);
|
|
|
|
static void append_special_headers(ResponseBuilder *b)
|
|
{
|
|
if (should_keep_alive(b->conn))
|
|
add_header(b, LIT("Connection: Keep-Alive"));
|
|
else {
|
|
add_header(b, LIT("Connection: Close"));
|
|
b->conn->closing = true;
|
|
b->conn->start_time = now;
|
|
}
|
|
|
|
b->content_length_offset = byte_queue_size(&b->conn->output) + sizeof("Content-Length: ") - 1;
|
|
add_header(b, LIT("Content-Length: "));
|
|
if (!byte_queue_write(&b->conn->output, LIT("\r\n")))
|
|
b->failed = true;
|
|
b->content_offset = byte_queue_size(&b->conn->output);
|
|
}
|
|
|
|
void append_content_s(ResponseBuilder *b, string str)
|
|
{
|
|
if (b->state == R_HEADER) {
|
|
append_special_headers(b);
|
|
b->state = R_CONTENT;
|
|
}
|
|
if (b->state != R_CONTENT)
|
|
log_fatal(LIT("Invalid response builder state\n"));
|
|
|
|
if (b->failed)
|
|
return;
|
|
|
|
if (!byte_queue_write(&b->conn->output, str)) {
|
|
b->failed = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
string append_content_start(ResponseBuilder *b, size_t cap)
|
|
{
|
|
if (b->state == R_HEADER) {
|
|
append_special_headers(b);
|
|
b->state = R_CONTENT;
|
|
}
|
|
if (b->state != R_CONTENT)
|
|
log_fatal(LIT("Invalid response builder state\n"));
|
|
|
|
if (b->failed)
|
|
return NULLSTR;
|
|
|
|
if (!byte_queue_ensure_min_free_space(&b->conn->output, cap)) {
|
|
b->failed = true;
|
|
return NULLSTR;
|
|
}
|
|
return byte_queue_start_write(&b->conn->output);
|
|
}
|
|
|
|
void append_content_end(ResponseBuilder *b, size_t num)
|
|
{
|
|
byte_queue_end_write(&b->conn->output, num);
|
|
}
|
|
|
|
void append_content_f(ResponseBuilder *b, const char *fmt, ...)
|
|
{
|
|
size_t cap = 128;
|
|
|
|
for (;;) {
|
|
|
|
string dst = append_content_start(b, cap);
|
|
if (dst.size == 0)
|
|
return;
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
int num = vsnprintf(dst.data, dst.size, fmt, args);
|
|
assert(num >= 0);
|
|
|
|
va_end(args);
|
|
|
|
if ((size_t) num < cap) {
|
|
append_content_end(b, num);
|
|
break;
|
|
}
|
|
|
|
cap *= 2;
|
|
}
|
|
}
|
|
|
|
bool append_file(ResponseBuilder *b, string file)
|
|
{
|
|
char copy[1<<12];
|
|
if (file.size >= sizeof(copy))
|
|
return false;
|
|
memcpy(copy, file.data, file.size);
|
|
copy[file.size] = '\0';
|
|
|
|
struct stat buf;
|
|
if (stat(copy, &buf))
|
|
return false;
|
|
|
|
if (!S_ISREG(buf.st_mode))
|
|
return false;
|
|
|
|
int fd;
|
|
do
|
|
fd = open(copy, O_RDONLY);
|
|
while (fd < 0 && errno == EINTR);
|
|
|
|
if (fd < 0)
|
|
return true;
|
|
|
|
string dst = append_content_start(b, (size_t) buf.st_size);
|
|
if (dst.size == 0) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
assert(dst.size >= (size_t) buf.st_size);
|
|
|
|
size_t copied = 0;
|
|
while (copied < (size_t) buf.st_size) {
|
|
int num = read(fd, dst.data + copied, (size_t) buf.st_size - copied);
|
|
if (num <= 0) {
|
|
if (num < 0)
|
|
return false;
|
|
break;
|
|
}
|
|
copied += num;
|
|
}
|
|
|
|
append_content_end(b, copied);
|
|
close(fd);
|
|
return true;
|
|
}
|
|
|
|
static void response_builder_complete(ResponseBuilder *b)
|
|
{
|
|
if (b->state == R_COMPLETE)
|
|
return;
|
|
|
|
if (b->failed)
|
|
return;
|
|
|
|
if (b->state == R_HEADER) {
|
|
append_special_headers(b);
|
|
if (b->failed) return;
|
|
} else {
|
|
if (b->state != R_CONTENT)
|
|
log_fatal(LIT("Invalid response builder state\n"));
|
|
}
|
|
size_t current_offset = byte_queue_size(&b->conn->output);
|
|
size_t content_length = current_offset - b->content_offset;
|
|
|
|
if (content_length > 1<<30) {
|
|
// Content larger than 1GB
|
|
b->failed = true;
|
|
return;
|
|
}
|
|
int content_length_int = (int) content_length;
|
|
|
|
char content_length_string[128];
|
|
int n = snprintf(content_length_string, sizeof(content_length_string), "%d", content_length_int);
|
|
assert(n >= 1 && n <= 9);
|
|
|
|
byte_queue_patch(&b->conn->output, b->content_length_offset, content_length_string, n);
|
|
|
|
b->state = R_COMPLETE;
|
|
}
|
|
|
|
static bool should_keep_alive(Connection *conn)
|
|
{
|
|
// Don't keep alive if the peer doesn't want to
|
|
if (conn->keep_alive == false)
|
|
return false;
|
|
|
|
// Don't keep alive if the request is too old
|
|
if (now - conn->creation_time > (uint64_t) connection_timeout_sec * 1000)
|
|
return false;
|
|
|
|
// Don't keep alive if we served a lot of requests to this connection
|
|
if (conn->served_count > keep_alive_max_requests)
|
|
return false;
|
|
|
|
// Don't keep alive if the server is more than 70% full
|
|
if (num_conns > 0.7 * max_conns)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static uint64_t deadline_of(Connection *conn)
|
|
{
|
|
return conn->start_time + (conn->closing ? closing_timeout_sec : request_timeout_sec) * 1000;
|
|
}
|
|
|
|
static bool respond_to_available_requests(Connection *conn)
|
|
{
|
|
bool remove = false;
|
|
|
|
int pipeline_count = 0;
|
|
while (!remove) { /* Respond loop start */
|
|
|
|
string src = byte_queue_start_read(&conn->input);
|
|
|
|
// Look for the \r\n\r\n
|
|
size_t j = 0;
|
|
while (j+3 < src.size && (src.data[j] != '\r' || src.data[j+1] != '\n' || src.data[j+2] != '\r' || src.data[j+3] != '\n'))
|
|
j++;
|
|
if (j+3 >= src.size)
|
|
break; // No \r\n\r\n
|
|
|
|
size_t head_length = j+4;
|
|
|
|
if (show_requests) {
|
|
print_bytes(LIT(""), (string) {src.data, head_length});
|
|
log_data(LIT("\n"));
|
|
}
|
|
|
|
// Found! We got the request head
|
|
|
|
Request request;
|
|
int res = parse_request_head((string) {src.data, head_length}, &request);
|
|
|
|
if (access_log) {
|
|
// Log access
|
|
time_t real_now_in_secs = real_now / 1000;
|
|
struct tm timeinfo;
|
|
localtime_r(&real_now_in_secs, &timeinfo);
|
|
char timebuf[128];
|
|
size_t timelen = strftime(timebuf, sizeof(timebuf), "%Y/%m/%d %H:%M:%S", &timeinfo);
|
|
if (timelen == 0)
|
|
log_fatal(LIT("Couldn't format time for access log"));
|
|
timebuf[timelen] = '\0';
|
|
|
|
char ipbuf[INET_ADDRSTRLEN];
|
|
const char *ipstr = inet_ntop(AF_INET, &conn->ipaddr, ipbuf, sizeof(ipbuf));
|
|
if (ipstr == NULL)
|
|
log_fatal(LIT("Couldn't format IP address for access log"));
|
|
|
|
if (res == P_OK) {
|
|
string user_agent;
|
|
if (!find_header(&request, LIT("User-Agent"), &user_agent))
|
|
user_agent = LIT("No User-Agent");
|
|
else
|
|
user_agent = trim(user_agent);
|
|
log_format("%s - %s - %.*s - %.*s\n", timebuf, ipstr,
|
|
(int) request.url.path.size, request.url.path.data,
|
|
(int) user_agent.size, user_agent.data);
|
|
} else {
|
|
log_format("%s - %s - Bad request\n", timebuf, ipstr);
|
|
}
|
|
}
|
|
|
|
if (res != P_OK) {
|
|
// Invalid HTTP request
|
|
byte_queue_write(&conn->output, LIT(
|
|
"HTTP/1.1 400 Bad Request\r\n"
|
|
"Connection: Close\r\n"
|
|
"\r\n"));
|
|
conn->closing = true;
|
|
conn->start_time = now;
|
|
break;
|
|
}
|
|
|
|
string content_length_header;
|
|
size_t content_length;
|
|
if (!find_header(&request, LIT("Content-Length"), &content_length_header)) {
|
|
|
|
if (find_and_parse_transfer_encoding(&request) & T_CHUNKED) {
|
|
// Content-Length missing
|
|
byte_queue_write(&conn->output, LIT(
|
|
"HTTP/1.1 411 Length Required\r\n"
|
|
"Connection: Close\r\n"
|
|
"\r\n"));
|
|
conn->closing = true;
|
|
conn->start_time = now;
|
|
log_data(LIT("Content-Length missing\n"));
|
|
break;
|
|
} else
|
|
content_length = 0;
|
|
|
|
} else {
|
|
content_length = parse_content_length(content_length_header);
|
|
if (content_length == (size_t) -1) {
|
|
// Invalid Content-Length
|
|
byte_queue_write(&conn->output, LIT(
|
|
"HTTP/1.1 400 Bad Request\r\n"
|
|
"Connection: Close\r\n"
|
|
"\r\n"));
|
|
conn->closing = true;
|
|
conn->start_time = now;
|
|
log_data(LIT("Invalid Content-Length\n"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (content_length > 1<<20) {
|
|
// Request too large
|
|
byte_queue_write(&conn->output, LIT(
|
|
"HTTP/1.1 413 Content Too Large\r\n"
|
|
"Connection: Close\r\n"
|
|
"\r\n"));
|
|
conn->closing = true;
|
|
conn->start_time = now;
|
|
log_data(LIT("Request too large\n"));
|
|
break;
|
|
}
|
|
|
|
size_t request_length = head_length + content_length;
|
|
if (src.size < request_length)
|
|
break; // Request wasn't completely received yet
|
|
request.content.data = src.data + head_length;
|
|
request.content.size = content_length;
|
|
|
|
// Reset the request timer
|
|
conn->start_time = now;
|
|
|
|
conn->keep_alive = true;
|
|
string keep_alive_header;
|
|
if (find_header(&request, LIT("Connection"), &keep_alive_header)) {
|
|
if (string_match_case_insensitive(trim(keep_alive_header), LIT("Close")))
|
|
conn->keep_alive = false;
|
|
}
|
|
// Respond
|
|
ResponseBuilder builder;
|
|
response_builder_init(&builder, conn);
|
|
assert(respond_callback);
|
|
respond_callback(request, &builder);
|
|
response_builder_complete(&builder);
|
|
if (builder.failed)
|
|
remove = true;
|
|
else {
|
|
conn->served_count++;
|
|
byte_queue_end_read(&conn->input, request_length);
|
|
if (!conn->keep_alive) {
|
|
conn->closing = true;
|
|
conn->start_time = now;
|
|
}
|
|
|
|
pipeline_count++;
|
|
if (pipeline_count == 10) {
|
|
// TODO: We should send a response to the client instead of dropping it
|
|
log_data(LIT("Pipeline limit reached\n"));
|
|
remove = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return remove;
|
|
}
|
|
|
|
void build_poll_array(struct pollfd *pollarray, int *timeout)
|
|
{
|
|
pollarray[0].fd = insecure_fd;
|
|
pollarray[0].events = (num_conns < max_conns ? POLLIN : 0);
|
|
pollarray[0].revents = 0;
|
|
|
|
#if HTTPS
|
|
pollarray[1].fd = secure_fd;
|
|
pollarray[1].events = (num_conns < max_conns ? POLLIN : 0);
|
|
pollarray[1].revents = 0;
|
|
#else
|
|
pollarray[1].fd = -1;
|
|
pollarray[1].events = 0;
|
|
pollarray[1].revents = 0;
|
|
#endif
|
|
|
|
Connection *oldest = NULL;
|
|
|
|
for (int i = 0; i < max_conns; i++) {
|
|
|
|
Connection *conn = &conns[i];
|
|
|
|
int events = 0;
|
|
|
|
if (conn->fd == -1) {
|
|
pollarray[i+2].fd = -1;
|
|
pollarray[i+2].events = 0;
|
|
pollarray[i+2].revents = 0;
|
|
continue;
|
|
}
|
|
|
|
if (conn->https) {
|
|
#if HTTPS
|
|
int state = br_ssl_engine_current_state(&conn->https_context.eng);
|
|
if (state & BR_SSL_SENDREC) events |= POLLOUT;
|
|
if (state & BR_SSL_RECVREC) events |= POLLIN;
|
|
#endif
|
|
} else {
|
|
if (byte_queue_size(&conn->output) > 0)
|
|
events |= POLLOUT;
|
|
if (!conn->closing)
|
|
events |= POLLIN;
|
|
}
|
|
|
|
pollarray[i+2].fd = conn->fd;
|
|
pollarray[i+2].events = events;
|
|
pollarray[i+2].revents = 0;
|
|
|
|
if (oldest == NULL || deadline_of(oldest) > deadline_of(conn)) oldest = conn;
|
|
}
|
|
|
|
if (oldest == NULL)
|
|
*timeout = -1;
|
|
else {
|
|
if (deadline_of(oldest) < now)
|
|
*timeout = 0;
|
|
else
|
|
*timeout = deadline_of(oldest) - now;
|
|
}
|
|
}
|
|
|
|
void init_connection(Connection *conn, int fd, uint32_t ipaddr, bool https)
|
|
{
|
|
byte_queue_init(&conn->input);
|
|
byte_queue_init(&conn->output);
|
|
conn->fd = fd;
|
|
conn->ipaddr = ipaddr;
|
|
conn->closing = false;
|
|
conn->https = https;
|
|
conn->served_count = 0;
|
|
conn->creation_time = now;
|
|
conn->start_time = now;
|
|
#if HTTPS
|
|
if (https) {
|
|
if (pkey.type == BR_KEYTYPE_RSA)
|
|
br_ssl_server_init_full_rsa(&conn->https_context, certs.items, certs.count, &pkey.rsa);
|
|
else {
|
|
assert(pkey.type == BR_KEYTYPE_EC);
|
|
unsigned issuer_key_type = BR_KEYTYPE_RSA; // Not sure if this or BR_KEYTYPE_EC
|
|
br_ssl_server_init_full_ec(&conn->https_context, certs.items, certs.count, issuer_key_type, &pkey.ec);
|
|
}
|
|
br_ssl_engine_set_versions(&conn->https_context.eng, BR_TLS10, BR_TLS12);
|
|
br_ssl_engine_set_buffer(&conn->https_context.eng, conn->https_buffer, sizeof(conn->https_buffer), 1);
|
|
br_ssl_server_reset(&conn->https_context);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void free_connection(Connection *conn)
|
|
{
|
|
assert(conn->fd != -1);
|
|
close(conn->fd);
|
|
byte_queue_free(&conn->input);
|
|
byte_queue_free(&conn->output);
|
|
conn->fd = -1;
|
|
conn->start_time = -1;
|
|
conn->closing = false;
|
|
conn->creation_time = 0;
|
|
}
|
|
|
|
bool accept_connection(int listen_fd, bool https)
|
|
{
|
|
// Look for a connection structure
|
|
int index = 0;
|
|
while (index < max_conns && conns[index].fd != -1)
|
|
index++;
|
|
if (index == max_conns)
|
|
return false; // Stop listening for incoming connections
|
|
|
|
struct sockaddr_in accepted_addr;
|
|
socklen_t accepted_addrlen = sizeof(accepted_addr);
|
|
int accepted_fd = accept(listen_fd, (struct sockaddr*) &accepted_addr, &accepted_addrlen);
|
|
if (accepted_fd < 0) {
|
|
if (errno == EINTR)
|
|
return true;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
|
return false;
|
|
log_perror(LIT("accept"));
|
|
return false;
|
|
}
|
|
uint32_t ipaddr = (uint32_t) accepted_addr.sin_addr.s_addr;
|
|
|
|
if (!set_blocking(accepted_fd, false)) {
|
|
log_perror(LIT("fcntl"));
|
|
close(accepted_fd);
|
|
return true;
|
|
}
|
|
|
|
Connection *conn = &conns[index];
|
|
init_connection(conn, accepted_fd, ipaddr, https);
|
|
|
|
assert(num_conns < max_conns);
|
|
num_conns++;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Returns true iff the connection should be dropped
|
|
bool update_connection_http(Connection *conn, struct pollfd *polldata)
|
|
{
|
|
// POLLIN
|
|
if ((!conn->closing) && (polldata->revents & (POLLIN | POLLHUP | POLLERR))) {
|
|
|
|
if (read_from_socket(conn->fd, &conn->input))
|
|
return true;
|
|
if (respond_to_available_requests(conn))
|
|
return true;
|
|
}
|
|
|
|
// POLLOUT
|
|
if (polldata->revents & POLLOUT) {
|
|
if (write_to_socket(conn->fd, &conn->output))
|
|
return true;
|
|
if (byte_queue_size(&conn->output) == 0 && conn->closing)
|
|
return true;
|
|
}
|
|
|
|
return false; // Don't close
|
|
}
|
|
|
|
#if HTTPS
|
|
// Returns true iff the connection should be dropped
|
|
bool update_connection_https(Connection *conn, struct pollfd *polldata)
|
|
{
|
|
br_ssl_engine_context *cc = &conn->https_context.eng;
|
|
bool flushed = false;
|
|
|
|
for (;;) {
|
|
|
|
int state = br_ssl_engine_current_state(cc);
|
|
|
|
if (state & BR_SSL_CLOSED) {
|
|
// Engine is finished, no more I/O (until next reset).
|
|
int error = br_ssl_engine_last_error(cc);
|
|
if (error != BR_ERR_OK) {
|
|
BearSSLErrorInfo error_info = get_bearssl_error_info(error);
|
|
log_format("SSL failure: %.*s (%.*s)\n",
|
|
(int) error_info.name.size, error_info.name.data,
|
|
(int) error_info.comment.size, error_info.comment.data);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if ((state & BR_SSL_SENDREC) && (polldata->revents & POLLOUT)) {
|
|
// Engine has some bytes to send to the peer
|
|
size_t len;
|
|
unsigned char *buf = br_ssl_engine_sendrec_buf(cc, &len);
|
|
size_t copied = 0;
|
|
while (copied < len) {
|
|
int num = send(conn->fd, buf + copied, len - copied, 0);
|
|
if (num < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
polldata->revents &= ~POLLOUT;
|
|
break;
|
|
}
|
|
log_perror(LIT("send"));
|
|
return true;
|
|
}
|
|
// TODO: Handle num=0
|
|
copied += (size_t) num;
|
|
}
|
|
br_ssl_engine_sendrec_ack(cc, copied);
|
|
flushed = false;
|
|
}
|
|
|
|
if ((state & BR_SSL_RECVAPP)) {
|
|
// Engine has obtained some application data from the
|
|
// peer, that should be read by the caller.
|
|
size_t len;
|
|
unsigned char *buf = br_ssl_engine_recvapp_buf(cc, &len);
|
|
if (!byte_queue_ensure_min_free_space(&conn->input, len))
|
|
return true;
|
|
string dst = byte_queue_start_write(&conn->input);
|
|
assert(dst.size >= len);
|
|
memcpy(dst.data, buf, len);
|
|
|
|
if (show_io)
|
|
print_bytes(LIT("> "), (string) {dst.data, len});
|
|
|
|
byte_queue_end_write(&conn->input, len);
|
|
br_ssl_engine_recvapp_ack(cc, len);
|
|
if (respond_to_available_requests(conn))
|
|
return true;
|
|
flushed = false;
|
|
}
|
|
|
|
if ((state & BR_SSL_RECVREC) && (polldata->revents & POLLIN)) {
|
|
// Engine expects some bytes from the peer
|
|
size_t len;
|
|
unsigned char *buf = br_ssl_engine_recvrec_buf(cc, &len);
|
|
size_t copied = 0;
|
|
while (copied < len) {
|
|
int num = recv(conn->fd, buf + copied, len - copied, 0);
|
|
if (num < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
polldata->revents &= ~POLLIN;
|
|
break;
|
|
}
|
|
log_perror(LIT("recv"));
|
|
return true;
|
|
}
|
|
if (num == 0) {
|
|
return true;
|
|
}
|
|
// TODO: Handle num=0
|
|
copied += (size_t) num;
|
|
}
|
|
br_ssl_engine_recvrec_ack(cc, copied);
|
|
flushed = false;
|
|
}
|
|
|
|
if ((state & BR_SSL_SENDAPP) && byte_queue_size(&conn->output) > 0) {
|
|
// Engine may receive application data to send (or flush).
|
|
size_t len;
|
|
unsigned char *buf = br_ssl_engine_sendapp_buf(cc, &len);
|
|
string src = byte_queue_start_read(&conn->output);
|
|
size_t copy = MIN(len, src.size);
|
|
memcpy(buf, src.data, copy);
|
|
|
|
if (show_io)
|
|
print_bytes(LIT("< "), (string) {src.data, copy});
|
|
|
|
byte_queue_end_read(&conn->output, copy);
|
|
br_ssl_engine_sendapp_ack(cc, copy);
|
|
br_ssl_engine_flush(cc, 0); // TODO: Is this the right time to call it?
|
|
flushed = false;
|
|
}
|
|
|
|
if (flushed) break;
|
|
br_ssl_engine_flush(cc, 0);
|
|
flushed = true;
|
|
}
|
|
|
|
int state = br_ssl_engine_current_state(cc);
|
|
if ((state & BR_SSL_SENDREC) == 0 && conn->closing && byte_queue_size(&conn->output) == 0)
|
|
return true;
|
|
|
|
return false; // Don't remove
|
|
}
|
|
#endif
|
|
|
|
bool update_connection(Connection *conn, struct pollfd *polldata)
|
|
{
|
|
bool ok;
|
|
|
|
#if HTTPS
|
|
if (conn->https)
|
|
ok = update_connection_https(conn, polldata);
|
|
else
|
|
#endif
|
|
ok = update_connection_http(conn, polldata);
|
|
|
|
return ok;
|
|
}
|
|
|
|
void http_stop(void)
|
|
{
|
|
stop = 1;
|
|
}
|
|
|
|
HTTPConfig http_default_config(void)
|
|
{
|
|
return (HTTPConfig) {
|
|
.http_port = 8080,
|
|
.http_addr = LIT("127.0.0.1"),
|
|
.https_port = 8081,
|
|
.https_addr = LIT("127.0.0.1"),
|
|
.cert_file = NULLSTR,
|
|
.privkey_file = NULLSTR,
|
|
.access_log = true,
|
|
.show_io = false,
|
|
.show_requests = false,
|
|
.max_connections = 512,
|
|
.keep_alive_max_requests = 1000,
|
|
.connection_timeout_sec = 60,
|
|
.closing_timeout_sec = 2,
|
|
.request_timeout_sec = 5,
|
|
.log_flush_timeout_sec = 3,
|
|
.respond = NULL,
|
|
};
|
|
}
|
|
|
|
void http_init(HTTPConfig config)
|
|
{
|
|
show_io = config.show_io;
|
|
show_requests = config.show_requests;
|
|
access_log = config.access_log;
|
|
keep_alive_max_requests = config.keep_alive_max_requests;
|
|
connection_timeout_sec = config.connection_timeout_sec;
|
|
closing_timeout_sec = config.closing_timeout_sec;
|
|
request_timeout_sec = config.request_timeout_sec;
|
|
log_flush_timeout_sec = config.log_flush_timeout_sec;
|
|
respond_callback = config.respond;
|
|
|
|
{
|
|
struct rlimit file_desc_limit;
|
|
if (getrlimit(RLIMIT_NOFILE, &file_desc_limit))
|
|
log_fatal(LIT("Couldn't query RLIMIT_NOFILE\n"));
|
|
|
|
max_conns = config.max_connections;
|
|
num_conns = 0;
|
|
if ((size_t) max_conns+2 > file_desc_limit.rlim_cur)
|
|
log_fatal(LIT("max_connections+2 is higher than the rlimit\n"));
|
|
|
|
conns = mymalloc(max_conns * sizeof(Connection));
|
|
if (conns == NULL)
|
|
log_fatal(LIT("Out of memory"));
|
|
|
|
for (int i = 0; i < max_conns; i++) {
|
|
conns[i].fd = -1;
|
|
byte_queue_init(&conns[i].input);
|
|
byte_queue_init(&conns[i].output);
|
|
}
|
|
|
|
pollarray = mymalloc((max_conns+2) * sizeof(struct pollfd));
|
|
if (pollarray == NULL)
|
|
log_fatal(LIT("Out of memory"));
|
|
|
|
DEBUG("Connection array created\n");
|
|
}
|
|
|
|
// Create plain text listener
|
|
{
|
|
string http_addr = config.http_addr;
|
|
uint32_t http_port = config.http_port;
|
|
insecure_fd = create_listening_socket(http_addr, http_port);
|
|
if (insecure_fd < 0)
|
|
log_fatal(LIT("Couldn't bind\n"));
|
|
log_format("Listening on %.*s:%d\n", (int) http_addr.size, http_addr.data, http_port);
|
|
DEBUG("HTTP started\n");
|
|
}
|
|
|
|
// Create secure listener
|
|
{
|
|
secure_fd = -1;
|
|
#if HTTPS
|
|
string https_addr = config.https_addr;
|
|
uint32_t https_port = config.https_port;
|
|
string https_cert_file = config.cert_file;
|
|
string https_key_file = config.privkey_file;
|
|
|
|
secure_fd = create_listening_socket(https_addr, https_port);
|
|
if (secure_fd < 0)
|
|
log_fatal(LIT("Couldn't bind\n"));
|
|
log_format("Listening on %.*s:%d\n", (int) https_addr.size, https_addr.data, https_port);
|
|
|
|
// Load certificate
|
|
if (!load_certs_from_file(https_cert_file, &certs))
|
|
log_fatal(LIT("Couldn't load certificates\n"));
|
|
DEBUG("Certificates loaded\n");
|
|
|
|
// Load private key
|
|
if (!load_private_key_from_file(https_key_file, &pkey))
|
|
log_fatal(LIT("Couldn't load private key\n"));
|
|
DEBUG("Private key loaded\n");
|
|
|
|
DEBUG("HTTPS started\n");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void http_loop(void)
|
|
{
|
|
uint64_t last_log_time = 0;
|
|
while (!stop) {
|
|
|
|
int timeout;
|
|
build_poll_array(pollarray, &timeout);
|
|
|
|
if (!log_empty()) {
|
|
int log_timeout = (last_log_time + log_flush_timeout_sec * 1000) - now;
|
|
if (timeout < 0)
|
|
timeout = log_timeout;
|
|
else
|
|
timeout = MIN(log_timeout, timeout);
|
|
}
|
|
|
|
int ret = poll(pollarray, max_conns+2, timeout);
|
|
if (ret < 0) {
|
|
if (errno == EINTR)
|
|
break; // TODO: Should this be continue?
|
|
log_perror(LIT("poll"));
|
|
exit(-1);
|
|
}
|
|
|
|
now = get_monotonic_time_ms();
|
|
real_now = get_real_time_ms();
|
|
|
|
if (pollarray[0].revents & POLLIN)
|
|
while (accept_connection(insecure_fd, false));
|
|
|
|
#if HTTPS
|
|
if (pollarray[1].revents & POLLIN)
|
|
while (accept_connection(secure_fd, true));
|
|
#endif
|
|
|
|
for (int i = 0; i < max_conns; i++) {
|
|
|
|
Connection *conn = &conns[i];
|
|
if (conn->fd == -1)
|
|
continue;
|
|
|
|
struct pollfd *polldata = &pollarray[i+2];
|
|
bool remove = false;
|
|
|
|
if (now >= deadline_of(conn)) {
|
|
|
|
assert(!remove);
|
|
|
|
if (conn->closing) {
|
|
// Closing timeout
|
|
remove = true;
|
|
log_data(LIT("Closing timeout\n"));
|
|
} else {
|
|
// Request timeout
|
|
if (byte_queue_size(&conn->input) == 0) {
|
|
// Connection was idle, so just close it
|
|
remove = true;
|
|
log_data(LIT("Idle connection timeout\n"));
|
|
} else {
|
|
byte_queue_write(&conn->output, LIT(
|
|
"HTTP/1.1 408 Request Timeout\r\n"
|
|
"Connection: Close\r\n"
|
|
"\r\n"));
|
|
conn->closing = true;
|
|
conn->start_time = now;
|
|
log_data(LIT("Request timeout\n"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!remove)
|
|
remove = update_connection(conn, polldata);
|
|
|
|
if (remove) {
|
|
free_connection(conn);
|
|
num_conns--;
|
|
}
|
|
}
|
|
|
|
if (now - last_log_time > (uint64_t) log_flush_timeout_sec * 1000) {
|
|
log_flush();
|
|
last_log_time = now;
|
|
}
|
|
|
|
} /* main loop end */
|
|
}
|
|
|
|
void http_free(void)
|
|
{
|
|
{
|
|
#if HTTPS
|
|
free_private_key(&pkey);
|
|
free_certs(&certs);
|
|
close(secure_fd);
|
|
#endif
|
|
|
|
close(insecure_fd);
|
|
|
|
for (int i = 0; i < max_conns; i++) {
|
|
if (conns[i].fd != -1) {
|
|
close(conns[i].fd);
|
|
byte_queue_free(&conns[i].input);
|
|
byte_queue_free(&conns[i].output);
|
|
}
|
|
}
|
|
myfree(conns, max_conns * sizeof(Connection));
|
|
myfree(pollarray, (max_conns+2) * sizeof(struct pollfd));
|
|
}
|
|
}
|
|
|
|
#define PATH_SEP '/'
|
|
|
|
int split_path_components(string src, string *stack, int limit, bool allow_ddots)
|
|
{
|
|
size_t cur = 0;
|
|
|
|
// Skip the first slash
|
|
if (cur < src.size && src.data[cur] == PATH_SEP)
|
|
cur++;
|
|
|
|
int depth = 0;
|
|
while (cur < src.size) {
|
|
|
|
if (depth == limit)
|
|
return -1;
|
|
|
|
size_t start = cur;
|
|
while (cur < src.size && (is_pcomp(src.data[cur]) || (allow_ddots && src.data[cur] == ':')))
|
|
cur++;
|
|
|
|
string comp = substr(src, start, cur);
|
|
|
|
if (comp.size == 0)
|
|
return -1; // We consider paths with empty components invalid
|
|
|
|
if (streq(comp, LIT(".."))) {
|
|
if (depth == 0)
|
|
return -1;
|
|
depth--;
|
|
} else {
|
|
if (!streq(comp, LIT(".")))
|
|
stack[depth++] = comp;
|
|
}
|
|
|
|
if (cur == src.size)
|
|
break;
|
|
|
|
if (src.data[cur] != PATH_SEP)
|
|
return -1;
|
|
cur++;
|
|
}
|
|
|
|
return depth;
|
|
}
|
|
|
|
/*
|
|
* Sanitize a path string removing ./ and ../
|
|
* components. The final path has an initial
|
|
* / but not final.
|
|
*/
|
|
size_t sanitize_path(string src, char *mem, size_t max)
|
|
{
|
|
#define MAX_COMPS 64
|
|
|
|
string stack[MAX_COMPS];
|
|
int depth;
|
|
|
|
depth = split_path_components(src, stack, MAX_COMPS, false);
|
|
if (depth < 0)
|
|
return -1;
|
|
|
|
/*
|
|
* Count how many output bytes are required
|
|
*/
|
|
size_t req = depth;
|
|
for (int i = 0; i < depth; i++)
|
|
req += stack[i].size;
|
|
if (req >= max)
|
|
return -1; // Buffer too small
|
|
|
|
/*
|
|
* Copy the sanitized path into the output
|
|
* buffer.
|
|
*/
|
|
size_t n = 0;
|
|
for (int i = 0; i < depth; i++) {
|
|
mem[n++] = PATH_SEP;
|
|
memcpy(mem + n, stack[i].data, stack[i].size);
|
|
n += stack[i].size;
|
|
}
|
|
mem[n] = '\0';
|
|
return n;
|
|
}
|
|
|
|
int match_path_format(string path, char *fmt, ...)
|
|
{
|
|
#define LIMIT 32
|
|
string p_stack[LIMIT];
|
|
string f_stack[LIMIT];
|
|
int p_depth;
|
|
int f_depth;
|
|
|
|
p_depth = split_path_components(path, p_stack, LIMIT, false);
|
|
f_depth = split_path_components(STR(fmt), f_stack, LIMIT, true);
|
|
|
|
if (p_depth < 0 || f_depth < 0)
|
|
return -1; // Error
|
|
|
|
if (p_depth != f_depth)
|
|
return 1; // No match
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
for (int i = 0; i < f_depth; i++) {
|
|
|
|
assert(f_stack[i].size > 0);
|
|
assert(p_stack[i].size > 0);
|
|
|
|
if (f_stack[i].data[0] == ':') {
|
|
if (f_stack[i].size != 2) {
|
|
va_end(args);
|
|
return -1; // Invalid format
|
|
}
|
|
switch (f_stack[i].data[1]) {
|
|
|
|
case 's':
|
|
{
|
|
string *sl = va_arg(args, string*);
|
|
*sl = p_stack[i];
|
|
}
|
|
break;
|
|
|
|
case 'n':
|
|
{
|
|
uint32_t n = 0;
|
|
size_t cur = 0;
|
|
while (cur < p_stack[i].size && is_digit(p_stack[i].data[cur])) {
|
|
int d = p_stack[i].data[cur] - '0';
|
|
if (n > (UINT32_MAX - d) / 10) {
|
|
va_end(args);
|
|
return -1; // Overflow
|
|
}
|
|
n = n * 10 + d;
|
|
cur++;
|
|
}
|
|
if (cur != p_stack[i].size) {
|
|
va_end(args);
|
|
return -1; // Component isn't a number
|
|
}
|
|
uint32_t *p = va_arg(args, uint32_t*);
|
|
*p = n;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
va_end(args);
|
|
return -1; // Invalid formt
|
|
}
|
|
} else {
|
|
if (f_stack[i].size != p_stack[i].size) {
|
|
va_end(args);
|
|
return 1; // No match
|
|
}
|
|
if (memcmp(f_stack[i].data, p_stack[i].data, f_stack[i].size)) {
|
|
va_end(args);
|
|
return 1; // No match
|
|
}
|
|
}
|
|
}
|
|
|
|
va_end(args);
|
|
return 0; // Match
|
|
}
|
|
|
|
struct {
|
|
string mime;
|
|
string ext;
|
|
} mime_table[] = {
|
|
{LIT("text/javascript"), LIT(".js")},
|
|
{LIT("text/javascript"), LIT(".javascript")},
|
|
{LIT("text/html"), LIT(".html")},
|
|
{LIT("text/html"), LIT(".htm")},
|
|
{LIT("image/gif"), LIT(".gif")},
|
|
{LIT("image/jpeg"), LIT(".jpg")},
|
|
{LIT("image/jpeg"), LIT(".jpeg")},
|
|
{LIT("image/svg+xml"), LIT(".svg")},
|
|
{LIT("video/mp4"), LIT(".mp4")},
|
|
{LIT("video/mpeg"), LIT(".mpeg")},
|
|
{LIT("font/ttf"), LIT(".ttf")},
|
|
{LIT("font/woff"), LIT(".woff")},
|
|
{LIT("font/woff2"), LIT(".woff2")},
|
|
{LIT("text/plain"), LIT(".txt")},
|
|
{LIT("audio/wav"), LIT(".wav")},
|
|
{LIT("application/x-7z-compressed"), LIT(".7z")},
|
|
{LIT("application/zip"), LIT(".zip")},
|
|
{LIT("application/xml"), LIT(".xml")},
|
|
{LIT("application/json"), LIT(".json")},
|
|
{NULLSTR, NULLSTR},
|
|
};
|
|
|
|
string mimetype_from_filename(string name)
|
|
{
|
|
for (size_t i = 0; i < COUNTOF(mime_table); i++)
|
|
if (endswith(mime_table[i].ext, name))
|
|
return mime_table[i].mime;
|
|
return NULLSTR;
|
|
}
|
|
|
|
bool serve_file_or_dir(ResponseBuilder *b, string prefix, string docroot,
|
|
string reqpath, string mime, bool enable_dir_listing)
|
|
{
|
|
// Sanitize the request path
|
|
char pathmem[1<<10];
|
|
string path;
|
|
{
|
|
size_t len = sanitize_path(reqpath, pathmem, sizeof(pathmem));
|
|
if (len >= sizeof(pathmem)) {
|
|
status_line(b, 500);
|
|
return true;
|
|
}
|
|
path = (string) {pathmem, len};
|
|
path.data[path.size] = '\0';
|
|
}
|
|
|
|
// Only handle this request if the prefix matches
|
|
if (!startswith(prefix, path))
|
|
return false;
|
|
|
|
// Remove the matched prefix and put the docroot in its place
|
|
{
|
|
if (docroot.size + path.size - prefix.size >= sizeof(pathmem)) {
|
|
status_line(b, 500);
|
|
return true;
|
|
}
|
|
memmove(pathmem + docroot.size, pathmem + prefix.size, path.size - prefix.size);
|
|
memcpy(pathmem, docroot.data, docroot.size);
|
|
path.size -= prefix.size;
|
|
path.size += docroot.size;
|
|
path.data[path.size] = '\0';
|
|
}
|
|
|
|
struct stat buf;
|
|
if (stat(path.data, &buf)) {
|
|
if (errno == ENOENT)
|
|
return false;
|
|
status_line(b, 500);
|
|
return true;
|
|
}
|
|
|
|
if (S_ISREG(buf.st_mode)) {
|
|
|
|
int fd;
|
|
do
|
|
fd = open(path.data, O_RDONLY);
|
|
while (fd < 0 && errno == EINTR);
|
|
|
|
if (fd < 0) {
|
|
status_line(b, 500);
|
|
close(fd);
|
|
return true;
|
|
}
|
|
|
|
status_line(b, 200);
|
|
|
|
if (mime.size == 0) mime = mimetype_from_filename(path);
|
|
if (mime.size > 0) add_header_f(b, "Content-Type: %.*s", (int) mime.size, mime.data);
|
|
|
|
string dst = append_content_start(b, (size_t) buf.st_size);
|
|
if (dst.size == 0) {
|
|
status_line(b, 500);
|
|
close(fd);
|
|
return true;
|
|
}
|
|
assert(dst.size >= (size_t) buf.st_size);
|
|
|
|
size_t copied = 0;
|
|
while (copied < (size_t) buf.st_size) {
|
|
int num = read(fd, dst.data + copied, (size_t) buf.st_size - copied);
|
|
if (num <= 0) {
|
|
if (num < 0)
|
|
log_format("Failed reading from '%.*s'\n", (int) path.size, path.data);
|
|
break;
|
|
}
|
|
copied += num;
|
|
}
|
|
|
|
append_content_end(b, copied);
|
|
close(fd);
|
|
return true;
|
|
}
|
|
|
|
if (enable_dir_listing && S_ISDIR(buf.st_mode)) {
|
|
|
|
DIR *d = opendir(path.data);
|
|
if (d == NULL) {
|
|
status_line(b, 500);
|
|
return true;
|
|
}
|
|
|
|
status_line(b, 200);
|
|
append_content_s(b, LIT(
|
|
"<html>\n"
|
|
" <head>\n"
|
|
" </head>\n"
|
|
" <body>\n"
|
|
" <ul>\n"
|
|
" <li><a href=\"\">(parent)</a></li>")); // TODO: Add links
|
|
|
|
struct dirent *dir;
|
|
while ((dir = readdir(d))) {
|
|
if (!strcmp(dir->d_name, ".") || !strcmp(dir->d_name, ".."))
|
|
continue;
|
|
append_content_f(b, "<li><a href=\"\">%s</a></li>\n", dir->d_name); // TODO: Add links
|
|
}
|
|
|
|
append_content_s(b, LIT(
|
|
" </ul>\n"
|
|
" </body>\n"
|
|
"</html>\n"));
|
|
closedir(d);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// BYTE QUEUE ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void byte_queue_init(ByteQueue *q)
|
|
{
|
|
q->data = NULL;
|
|
q->head = 0;
|
|
q->size = 0;
|
|
q->capacity = 0;
|
|
}
|
|
|
|
void byte_queue_free(ByteQueue *q)
|
|
{
|
|
myfree(q->data, q->capacity);
|
|
byte_queue_init(q);
|
|
}
|
|
|
|
bool byte_queue_ensure_min_free_space(ByteQueue *q, size_t num)
|
|
{
|
|
size_t total_free_space = q->capacity - q->size;
|
|
size_t free_space_after_data = q->capacity - q->size - q->head;
|
|
|
|
if (free_space_after_data < num) {
|
|
if (total_free_space < num) {
|
|
// Resize required
|
|
|
|
size_t capacity = MAX(2 * q->capacity, q->size + num);
|
|
|
|
char *data = mymalloc(capacity);
|
|
if (!data) return false;
|
|
|
|
if (q->size > 0)
|
|
memcpy(data, q->data + q->head, q->size);
|
|
|
|
myfree(q->data, q->capacity);
|
|
|
|
q->data = data;
|
|
q->head = 0;
|
|
q->capacity = capacity;
|
|
|
|
} else {
|
|
// Move required
|
|
memmove(q->data, q->data + q->head, q->size);
|
|
q->head = 0;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
string byte_queue_start_write(ByteQueue *q)
|
|
{
|
|
if (q->data == NULL)
|
|
return NULLSTR;
|
|
return (string) {
|
|
.data = q->data + (q->head + q->size),
|
|
.size = q->capacity - (q->head + q->size),
|
|
};
|
|
}
|
|
|
|
void byte_queue_end_write(ByteQueue *q, size_t num)
|
|
{
|
|
q->size += num;
|
|
}
|
|
|
|
string byte_queue_start_read(ByteQueue *q)
|
|
{
|
|
if (q->data == NULL)
|
|
return NULLSTR;
|
|
return (string) {
|
|
.data = q->data + q->head,
|
|
.size = q->size,
|
|
};
|
|
}
|
|
|
|
size_t byte_queue_size(ByteQueue *q)
|
|
{
|
|
return q->size;
|
|
}
|
|
|
|
void byte_queue_end_read(ByteQueue *q, size_t num)
|
|
{
|
|
q->head += num;
|
|
q->size -= num;
|
|
}
|
|
|
|
bool byte_queue_write(ByteQueue *q, string src)
|
|
{
|
|
if (!byte_queue_ensure_min_free_space(q, src.size))
|
|
return false;
|
|
string dst = byte_queue_start_write(q);
|
|
assert(dst.size >= src.size);
|
|
memcpy(dst.data, src.data, src.size);
|
|
byte_queue_end_write(q, src.size);
|
|
return true;
|
|
}
|
|
|
|
void byte_queue_patch(ByteQueue *q, size_t offset, char *src, size_t len)
|
|
{
|
|
// TODO: Safety checks
|
|
memcpy(q->data + q->head + offset, src, len);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// SOCKET UTILITIES ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void print_bytes(string prefix, string str)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
bool line_start = true;
|
|
|
|
size_t i = 0;
|
|
while (i < len) {
|
|
|
|
size_t substr_offset = i;
|
|
while (i < len && src[i] != '\r' && src[i] != '\n')
|
|
i++;
|
|
size_t substr_length = i - substr_offset;
|
|
|
|
if (line_start) {
|
|
log_data(prefix);
|
|
line_start = false;
|
|
}
|
|
|
|
log_data((string) { src + substr_offset, substr_length });
|
|
|
|
if (i < len) {
|
|
if (src[i] == '\r')
|
|
log_data(LIT("\\r"));
|
|
else {
|
|
log_data(LIT("\\n\n"));
|
|
line_start = true;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (!line_start)
|
|
log_data(LIT("\n"));
|
|
}
|
|
|
|
bool read_from_socket(int fd, ByteQueue *queue)
|
|
{
|
|
bool remove = false;
|
|
|
|
for (;;) {
|
|
|
|
if (!byte_queue_ensure_min_free_space(queue, 512)) {
|
|
remove = true;
|
|
break;
|
|
}
|
|
|
|
string dst = byte_queue_start_write(queue);
|
|
|
|
int num = recv(fd, dst.data, dst.size, 0);
|
|
if (num < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
|
break;
|
|
log_perror(LIT("recv"));
|
|
remove = true;
|
|
break;
|
|
}
|
|
if (num == 0) {
|
|
remove = true;
|
|
break;
|
|
}
|
|
|
|
if (show_io)
|
|
print_bytes(LIT("> "), (string) {dst.data, num});
|
|
|
|
byte_queue_end_write(queue, (size_t) num);
|
|
|
|
// Input buffer can't go over 20Mb
|
|
if (byte_queue_size(queue) > (size_t) INPUT_BUFFER_LIMIT_MB * 1024 * 1024) {
|
|
remove = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return remove;
|
|
}
|
|
|
|
bool write_to_socket(int fd, ByteQueue *queue)
|
|
{
|
|
bool remove = false;
|
|
for (;;) {
|
|
|
|
string src = byte_queue_start_read(queue);
|
|
if (src.size == 0) break;
|
|
|
|
int num = send(fd, src.data, src.size, 0);
|
|
if (num < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
|
break;
|
|
log_perror(LIT("send"));
|
|
remove = true;
|
|
break;
|
|
}
|
|
|
|
if (show_io)
|
|
print_bytes(LIT("< "), (string) {src.data, num});
|
|
|
|
byte_queue_end_read(queue, (size_t) num);
|
|
}
|
|
|
|
return remove;
|
|
}
|
|
|
|
int create_listening_socket(string addr, int port)
|
|
{
|
|
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (listen_fd < 0) {
|
|
log_perror(LIT("socket"));
|
|
return -1;
|
|
}
|
|
|
|
if (!set_blocking(listen_fd, false)) {
|
|
log_perror(LIT("fcntl"));
|
|
return -1;
|
|
}
|
|
|
|
int one = 1;
|
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (char*) &one, sizeof(one));
|
|
|
|
struct in_addr addr2;
|
|
if (addr.size == 0)
|
|
addr2.s_addr = htonl(INADDR_ANY);
|
|
else {
|
|
assert(addr.data);
|
|
char addr_copy[INET_ADDRSTRLEN];
|
|
if (addr.size >= SIZEOF(addr_copy))
|
|
log_fatal(LIT("Invalid IP address (too long)\n"));
|
|
memcpy(addr_copy, addr.data, addr.size);
|
|
addr_copy[addr.size] = '\0';
|
|
int res = inet_pton(AF_INET, addr_copy, &addr2);
|
|
if (res == 0) log_fatal(LIT("Invalid IP address (too long)\n"));
|
|
if (res != 1) log_fatal(LIT("Could not parse IP address (internal error)\n"));
|
|
}
|
|
|
|
struct sockaddr_in addr3;
|
|
addr3.sin_family = AF_INET;
|
|
addr3.sin_port = htons(port);
|
|
addr3.sin_addr = addr2;
|
|
if (bind(listen_fd, (struct sockaddr*) &addr3, sizeof(addr3))) {
|
|
log_perror(LIT("bind"));
|
|
return -1;
|
|
}
|
|
|
|
if (listen(listen_fd, 32)) {
|
|
log_perror(LIT("listen"));
|
|
return -1;
|
|
}
|
|
|
|
return listen_fd;
|
|
}
|
|
|
|
bool set_blocking(int fd, bool blocking)
|
|
{
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
|
|
if (flags == -1)
|
|
return false;
|
|
|
|
if (blocking)
|
|
flags &= ~O_NONBLOCK;
|
|
else
|
|
flags |= O_NONBLOCK;
|
|
|
|
if (fcntl(fd, F_SETFL, flags))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// URI PARSER ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool is_lower_alpha(char c)
|
|
{
|
|
return c >= 'a' && c <= 'z';
|
|
}
|
|
|
|
static bool is_upper_alpha(char c)
|
|
{
|
|
return c >= 'A' && c <= 'Z';
|
|
}
|
|
|
|
static bool is_hex_digit(char c)
|
|
{
|
|
return is_digit(c)
|
|
|| (c >= 'a' && c <= 'f')
|
|
|| (c >= 'A' && c <= 'F');
|
|
}
|
|
|
|
static bool is_unreserved(char c)
|
|
{
|
|
return is_alpha(c) || is_digit(c)
|
|
|| c == '-' || c == '.'
|
|
|| c == '_' || c == '~';
|
|
}
|
|
|
|
static bool is_subdelim(char c)
|
|
{
|
|
return c == '!' || c == '$'
|
|
|| c == '&' || c == '\''
|
|
|| c == '(' || c == ')'
|
|
|| c == '*' || c == '+'
|
|
|| c == ',' || c == ';'
|
|
|| c == '=';
|
|
}
|
|
|
|
static bool is_pchar(char c)
|
|
{
|
|
return is_unreserved(c)
|
|
|| is_subdelim(c)
|
|
|| c == ':' || c == '@';
|
|
}
|
|
|
|
static bool is_schema_first(char c)
|
|
{
|
|
return is_alpha(c);
|
|
}
|
|
|
|
static bool is_schema(char c)
|
|
{
|
|
return is_alpha(c)
|
|
|| is_digit(c)
|
|
|| c == '+'
|
|
|| c == '-'
|
|
|| c == '.';
|
|
}
|
|
|
|
// Symbol: parse_schema
|
|
// Parse the schema of an url, if there is one.
|
|
//
|
|
// Arguments:
|
|
// (in) src: The source string.
|
|
//
|
|
// len: The number of bytes pointed by [src].
|
|
//
|
|
// (in/out) i: Offset from where the parsing should
|
|
// start. When the function returns, its
|
|
// value is incremented by the number of
|
|
// parsed bytes (therefore pointing to the
|
|
// first non-parsed byte). If the url in
|
|
// the source has no schema, it's value
|
|
// is left unchanged.
|
|
//
|
|
// (out) schema: Non zero-terminated string containing
|
|
// the parsed schema. If the url didn't
|
|
// have a schema, it's set to NULL.
|
|
//
|
|
// (out) schema_len: Length of the parsed schema, or 0 if
|
|
// there wasn't one.
|
|
//
|
|
// Returns:
|
|
// - [i] is incremented by the number of parsed bytes.
|
|
//
|
|
// - [schema] points to the schema string (or NULL if
|
|
// there wasn't one).
|
|
//
|
|
// - [schema_len] is the length of the string pointed
|
|
// by [schema], or 0 if there was no schema.
|
|
//
|
|
// Notes:
|
|
// - This function can never fail.
|
|
//
|
|
static void parse_schema(string str, size_t *i, string *schema)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t peek = *i; // Local cursor
|
|
|
|
bool no_schema;
|
|
size_t schema_off;
|
|
size_t schema_end;
|
|
|
|
// If there is a schema, set [no_schema] to
|
|
// [false], [schema_off] to the start of
|
|
// the schema substring (relative to [src])
|
|
// and [schema_end] to its end.
|
|
{
|
|
if (peek == len || !is_schema_first(src[peek]))
|
|
// The first character can't be the start
|
|
// of a schema.
|
|
no_schema = true;
|
|
else {
|
|
// The current character is a valid start
|
|
// for a schema. We'll assume it is until
|
|
// proven otherwise.
|
|
|
|
// Keep track of the current posizion, then
|
|
// consume all characters that compose the
|
|
// schema.
|
|
schema_off = peek;
|
|
do
|
|
peek++;
|
|
while (peek < len && is_schema(src[peek]));
|
|
schema_end = peek;
|
|
|
|
// If the character following the schema
|
|
// isn't a ':', it wasn't a schema after
|
|
// all.
|
|
if (peek == len || src[peek] != ':')
|
|
no_schema = true;
|
|
else {
|
|
no_schema = false;
|
|
peek++; // Skip the ':'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the output parameters.
|
|
if (no_schema)
|
|
*schema = NULLSTR;
|
|
// Don't unpdate [i]
|
|
else {
|
|
*schema = substr(str, schema_off, schema_end);
|
|
*i = peek; // Commit changes.
|
|
}
|
|
}
|
|
|
|
static bool is_username(char c)
|
|
{
|
|
return is_unreserved(c) || is_subdelim(c);
|
|
}
|
|
|
|
static bool is_username_first(char c)
|
|
{
|
|
return is_username(c);
|
|
}
|
|
|
|
static bool is_password(char c)
|
|
{
|
|
return is_username(c);
|
|
}
|
|
|
|
static bool is_password_first(char c)
|
|
{
|
|
return is_password(c);
|
|
}
|
|
|
|
// Symbol: parse_userinfo
|
|
// Parse the userinfo component of an URL,
|
|
// if there is one.
|
|
//
|
|
// The userinfo component is a subcomponent
|
|
// of the authority. It contains a username
|
|
// and, optionally, a password:
|
|
//
|
|
// http://username@example.com
|
|
// http://username:password@example.com
|
|
//
|
|
// This functions parses the portion that
|
|
// goes from the byte following "//" to
|
|
// the '@'.
|
|
//
|
|
// Arguments:
|
|
// (in) src: The source string.
|
|
//
|
|
// len: The number of bytes pointed by [src]
|
|
//
|
|
// (in/out) i: Offset from where the parsing should
|
|
// start. When the function returns, its
|
|
// value is incremented by the number of
|
|
// parsed bytes (therefore pointing to the
|
|
// first non-parsed byte). If the url in
|
|
// the source has no userinfo, it's value
|
|
// is left unchanged.
|
|
//
|
|
// (out) userinfo: The result of the parsing.
|
|
//
|
|
// Returns:
|
|
// - [i] is incremented by the number of parsed bytes.
|
|
//
|
|
// - [userinfo] contains the parsed username and
|
|
// password. These strings are not zero-terminated.
|
|
// If there was no userinfo component, then
|
|
// [userinfo.username] and [userinfo.password] are
|
|
// NULL and [userinfo.username_len] and
|
|
// [userinfo.password_len] are 0. If the username
|
|
// is specified but the password isn't, then only
|
|
// [userinfo.password] is NULL and
|
|
// [userinfo.password_len] is 0.
|
|
//
|
|
// Notes:
|
|
// - This function can never fail.
|
|
//
|
|
static void
|
|
parse_userinfo(string str, size_t *i, url_userinfo *userinfo)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t peek = *i; // Local cursor
|
|
|
|
bool no_username;
|
|
bool no_password;
|
|
size_t username_off;
|
|
size_t username_end;
|
|
size_t password_off;
|
|
size_t password_len;
|
|
|
|
// If the userinfo subcomponent is present,
|
|
// set [no_username] to false, [username_off]
|
|
// to the offset of the username relative to
|
|
// [src] and [username_end] to its end.
|
|
// If the password was also specified, do the
|
|
// same.
|
|
{
|
|
if (peek == len || !is_username_first(src[peek])) {
|
|
// The first character can't be the first
|
|
// of an username, therefore there's no
|
|
// userinfo subcomponent.
|
|
no_username = true;
|
|
no_password = true;
|
|
} else {
|
|
|
|
// The first character is a valid start
|
|
// for an username, therefore we'll assume
|
|
// that there's a userinfo subcomponent
|
|
// until proven otherwise.
|
|
|
|
// Scan the username while keeping track
|
|
// of it's offset and length.
|
|
username_off = peek;
|
|
do
|
|
peek++;
|
|
while (peek < len && is_username(src[peek]));
|
|
username_end = peek;
|
|
|
|
// If the username is followed by a ':' and
|
|
// a valid password character, we also expect
|
|
// a password.
|
|
if (peek+1 < len && src[peek] == ':' && is_password_first(src[peek+1])) {
|
|
|
|
peek++; // Skip the ':'
|
|
|
|
// Scan the password while keeping track
|
|
// of it's offset and length.
|
|
password_off = peek;
|
|
do
|
|
peek++;
|
|
while (peek < len && is_password(src[peek]));
|
|
password_len = peek;
|
|
|
|
if (peek == len || src[peek] != '@') {
|
|
// If the password isn't followed by a '@',
|
|
// then this wasn't a userinfo subcomponent
|
|
// after all.
|
|
no_username = true;
|
|
no_password = true;
|
|
} else {
|
|
// All done.
|
|
no_username = false;
|
|
no_password = false;
|
|
peek++; // Skip the '@'
|
|
}
|
|
|
|
} else if (peek == len || src[peek] != '@') {
|
|
// Since no password was specified, if the
|
|
// username isn't followed by a '@', then
|
|
// this wasn't a userinfo subcomponent after
|
|
// all.
|
|
no_username = true;
|
|
no_password = true;
|
|
} else {
|
|
// All done.
|
|
no_username = false;
|
|
no_password = true;
|
|
peek++; // Skip the '@'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the output parameters.
|
|
if (no_username) {
|
|
assert(no_password);
|
|
userinfo->username = NULLSTR;
|
|
userinfo->password = NULLSTR;
|
|
// Don't update [i]
|
|
} else {
|
|
userinfo->username = substr(str, username_off, username_end);
|
|
userinfo->password = no_password ? NULLSTR : substr(str, password_off, password_len);
|
|
*i = peek; // Commit changes.
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_ipv4_byte(string str, size_t *i, uint8_t *out)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t peek = *i;
|
|
|
|
// If the cursor refers to a digit, then
|
|
// there's a byte to parse.
|
|
bool start_with_digit = (peek < len && is_digit(src[peek]));
|
|
|
|
if (start_with_digit) {
|
|
|
|
uint8_t byte = 0;
|
|
|
|
// TODO: Don't allow arbitrary sequence of
|
|
// 0s at the start.
|
|
do {
|
|
int d = src[peek] - '0';
|
|
if (byte > (UINT8_MAX - d) / 10)
|
|
break; // Overflow! This digit isn't part of the byte.
|
|
byte = byte * 10 + d;
|
|
peek++;
|
|
} while (peek < len && is_digit(src[peek]));
|
|
|
|
*i = peek;
|
|
*out = byte;
|
|
}
|
|
|
|
return start_with_digit;
|
|
}
|
|
|
|
static uint32_t pack(uint8_t *bytes)
|
|
{
|
|
return
|
|
((uint32_t) bytes[0] << 24) |
|
|
((uint32_t) bytes[1] << 16) |
|
|
((uint32_t) bytes[2] << 8) |
|
|
((uint32_t) bytes[3] << 0);
|
|
}
|
|
|
|
static bool parse_ipv4(string str, size_t *i, uint32_t *ipv4)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t peek = *i;
|
|
uint8_t unpacked_ipv4[4];
|
|
|
|
for (int u = 0; u < 3; u++) {
|
|
|
|
if (!parse_ipv4_byte(str, &peek, unpacked_ipv4 + u))
|
|
return false;
|
|
|
|
if (peek == len || src[peek] != '.')
|
|
return false;
|
|
|
|
peek++; // Skip the dot
|
|
}
|
|
if (!parse_ipv4_byte(str, &peek, unpacked_ipv4 + 3))
|
|
return false;
|
|
|
|
*ipv4 = pack(unpacked_ipv4);
|
|
*i = peek;
|
|
return true;
|
|
}
|
|
|
|
static int hex_digit_to_int(char c)
|
|
{
|
|
assert(is_hex_digit(c));
|
|
|
|
if (is_lower_alpha(c))
|
|
return c - 'a' + 10;
|
|
|
|
if (is_upper_alpha(c))
|
|
return c - 'A' + 10;
|
|
|
|
assert(is_digit(c));
|
|
return c - '0';
|
|
}
|
|
|
|
static bool parse_ipv6_word(string str, size_t *i, uint16_t *out)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t peek = *i;
|
|
|
|
// If there's at least one hex digit at the
|
|
// current position, then we can parse a word
|
|
// for sure.
|
|
bool start_with_hex_digit = (peek < len && is_hex_digit(src[peek]));
|
|
|
|
if (start_with_hex_digit) {
|
|
|
|
uint16_t word = 0;
|
|
|
|
// TODO: Don't allow arbitrary sequence of
|
|
// 0s at the start.
|
|
do {
|
|
|
|
int d = hex_digit_to_int(src[peek]);
|
|
if (word > (UINT16_MAX - d) / 16)
|
|
break; // Overflow! This hex digit isn't part of the word.
|
|
|
|
word = word * 16 + d;
|
|
|
|
peek++;
|
|
|
|
} while (peek < len && is_hex_digit(src[peek]));
|
|
|
|
*i = peek;
|
|
*out = word;
|
|
}
|
|
|
|
return start_with_hex_digit;
|
|
}
|
|
|
|
static bool parse_ipv6(string str, size_t *i, uint16_t ipv6[static 8])
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t k = *i;
|
|
|
|
uint16_t tail[8];
|
|
size_t head_count = 0;
|
|
size_t tail_count = 0;
|
|
|
|
if (k+1 < len && src[k] == ':' && src[k+1] == ':')
|
|
k += 2;
|
|
else {
|
|
while (1) {
|
|
|
|
uint16_t word;
|
|
if (!parse_ipv6_word(str, &k, &word))
|
|
return false;
|
|
|
|
ipv6[head_count++] = word;
|
|
|
|
if (head_count == 8)
|
|
break;
|
|
|
|
if (k == len || src[k] != ':')
|
|
return false;
|
|
k++; // Skip the ':'
|
|
|
|
if (k < len && src[k] == ':') {
|
|
k++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (head_count + tail_count < 8) {
|
|
while (k < len && is_hex_digit(src[k])) {
|
|
|
|
// We know the current character is a
|
|
// hex digit, therefore [parse_ipv6_word]
|
|
// won't fail.
|
|
uint16_t word;
|
|
(void) parse_ipv6_word(str, &k, &word);
|
|
|
|
tail[tail_count++] = word;
|
|
|
|
if (head_count + tail_count == 7)
|
|
break;
|
|
|
|
if (k == len || src[k] != ':')
|
|
break;
|
|
k++; // Skip the ':'
|
|
}
|
|
}
|
|
|
|
for (size_t p = 0; p < 8 - head_count - tail_count; p++)
|
|
ipv6[head_count + p] = 0;
|
|
|
|
for (size_t p = 0; p < tail_count; p++)
|
|
ipv6[8 - tail_count + p] = tail[p];
|
|
|
|
*i = k;
|
|
return true;
|
|
}
|
|
|
|
static void parse_port(string str, size_t *i, bool *no_port, uint16_t *port)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t k = *i;
|
|
|
|
if (k+1 < len && src[k] == ':' && is_digit(src[k+1])) {
|
|
|
|
k++; // Skip the ':'
|
|
|
|
uint16_t p = 0;
|
|
do {
|
|
int d = src[k] - '0';
|
|
if (p > (UINT16_MAX - d) / 10)
|
|
break;
|
|
p = p * 10 + d;
|
|
k++;
|
|
} while (k < len && is_digit(src[k]));
|
|
|
|
*port = p;
|
|
*no_port = false;
|
|
} else {
|
|
*port = 0;
|
|
*no_port = true;
|
|
}
|
|
|
|
*i = k;
|
|
}
|
|
|
|
static bool is_hostname(char c)
|
|
{
|
|
return is_unreserved(c) || is_subdelim(c);
|
|
}
|
|
|
|
static bool is_hostname_first(char c)
|
|
{
|
|
return is_hostname(c);
|
|
}
|
|
|
|
static bool parse_host(string str, size_t *i, url_host *host)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t k = *i;
|
|
if (k == len)
|
|
return false;
|
|
|
|
if (src[k] == '[') {
|
|
|
|
k++; // Skip the '['
|
|
|
|
// IPv6
|
|
if (!parse_ipv6(str, &k, host->ipv6))
|
|
return false;
|
|
|
|
if (k == len || src[k] != ']')
|
|
return false;
|
|
k++; // Skip the ']'
|
|
|
|
host->mode = URL_HOSTMODE_IPV6;
|
|
|
|
} else {
|
|
|
|
uint32_t ipv4;
|
|
bool is_ipv4;
|
|
|
|
if (is_digit(src[k]))
|
|
is_ipv4 = parse_ipv4(str, &k, &ipv4);
|
|
else
|
|
is_ipv4 = false;
|
|
|
|
if (is_ipv4) {
|
|
host->ipv4 = ipv4;
|
|
host->mode = URL_HOSTMODE_IPV4;
|
|
} else {
|
|
|
|
if (!is_hostname_first(src[k]))
|
|
return false;
|
|
|
|
size_t name_off = k;
|
|
do
|
|
k++;
|
|
while (k < len && is_hostname(src[k]));
|
|
size_t name_end = k;
|
|
|
|
host->mode = URL_HOSTMODE_NAME;
|
|
host->name = substr(str, name_off, name_end);
|
|
}
|
|
}
|
|
|
|
uint16_t port;
|
|
bool no_port;
|
|
parse_port(str, &k, &no_port, &port);
|
|
host->port = port;
|
|
host->no_port = no_port;
|
|
*i = k;
|
|
return true;
|
|
}
|
|
|
|
static bool parse_path(string str, size_t *i, string *path)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t k = *i;
|
|
|
|
size_t path_off;
|
|
|
|
path_off = k;
|
|
|
|
if (k < len && src[k] == '/')
|
|
k++;
|
|
else {
|
|
if (k == len || !is_pchar(src[k]))
|
|
return false;
|
|
}
|
|
|
|
while (k < len && is_pchar(src[k])) {
|
|
do
|
|
k++;
|
|
while (k < len && is_pchar(src[k]));
|
|
if (k == len || src[k] != '/')
|
|
break;
|
|
k++; // Skip the '/'
|
|
}
|
|
|
|
*i = k;
|
|
*path = substr(str, path_off, k);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool is_query(char c)
|
|
{
|
|
return is_pchar(c) || c == '/' || c == '?';
|
|
}
|
|
|
|
static void parse_query(string str, size_t *i, string *query)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
bool no_query;
|
|
size_t query_off;
|
|
size_t query_end;
|
|
|
|
size_t peek = *i;
|
|
if (peek < len && src[peek] == '?') {
|
|
peek++; // Skip the '?'
|
|
query_off = peek;
|
|
while (peek < len && is_query(src[peek]))
|
|
peek++;
|
|
query_end = peek;
|
|
no_query = false;
|
|
} else {
|
|
query_off = 0;
|
|
query_end = 0;
|
|
no_query = true;
|
|
}
|
|
|
|
if (no_query)
|
|
*query = NULLSTR;
|
|
else {
|
|
*i = peek;
|
|
*query = substr(str, query_off, query_end);
|
|
}
|
|
}
|
|
|
|
static bool is_fragment(char c)
|
|
{
|
|
return is_pchar(c) || c == '/';
|
|
}
|
|
|
|
static void parse_fragment(string str, size_t *i, string *fragment)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
bool no_fragment;
|
|
size_t fragment_off;
|
|
size_t fragment_end;
|
|
|
|
size_t peek = *i;
|
|
if (peek < len && src[peek] == '#') {
|
|
peek++; // Skip the '#'
|
|
fragment_off = peek;
|
|
while (peek < len && is_fragment(src[peek]))
|
|
peek++;
|
|
fragment_end = peek;
|
|
no_fragment = false;
|
|
} else {
|
|
fragment_off = 0;
|
|
fragment_end = 0;
|
|
no_fragment = true;
|
|
}
|
|
|
|
if (no_fragment)
|
|
*fragment = NULLSTR;
|
|
else {
|
|
*i = peek;
|
|
*fragment = substr(str, fragment_off, fragment_end);
|
|
}
|
|
}
|
|
|
|
static bool follows_authority(string str, size_t i)
|
|
{
|
|
return i+1 < str.size
|
|
&& str.data[i] == '/'
|
|
&& str.data[i+1] == '/';
|
|
}
|
|
|
|
static bool url_parse2(string str, size_t *i, url_t *url)
|
|
{
|
|
char *src = str.data;
|
|
size_t len = str.size;
|
|
|
|
size_t maybe;
|
|
if (i == NULL) {
|
|
maybe = 0;
|
|
i = &maybe;
|
|
}
|
|
|
|
parse_schema(str, i, &url->schema);
|
|
|
|
if (follows_authority(str, *i)) {
|
|
|
|
*i += 2; // Skip the "//"
|
|
|
|
parse_userinfo(str, i, &url->userinfo);
|
|
|
|
if (!parse_host(str, i, &url->host))
|
|
return false;
|
|
|
|
if (*i < len && src[*i] == '/') {
|
|
/* absolute path */
|
|
|
|
// The parsing of the path can't fail
|
|
// because we already know there's at
|
|
// leat a '/' for it.
|
|
(void) parse_path(str, i, &url->path);
|
|
} else
|
|
url->path = NULLSTR;
|
|
|
|
} else {
|
|
|
|
url->host.mode = URL_HOSTMODE_NAME;
|
|
url->host.name = NULLSTR;
|
|
url->host.no_port = true;
|
|
url->host.port = 0;
|
|
|
|
url->userinfo.username = NULLSTR;
|
|
url->userinfo.password = NULLSTR;
|
|
|
|
// TODO: Since there was no authority,
|
|
// the path is non optional.
|
|
|
|
if (*i == len || src[*i] == '?' || src[*i] == '#')
|
|
return false; // Missing path
|
|
|
|
if (!parse_path(str, i, &url->path))
|
|
return false;
|
|
}
|
|
|
|
parse_query(str, i, &url->query);
|
|
parse_fragment(str, i, &url->fragment);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool url_parse(string str, url_t *url)
|
|
{
|
|
size_t i = 0;
|
|
bool result = url_parse2(str, &i, url);
|
|
return result && i == str.size;
|
|
}
|
|
|
|
static bool url_parse_ipv4(string str, uint32_t *out)
|
|
{
|
|
size_t i = 0;
|
|
return parse_ipv4(str, &i, out);
|
|
}
|
|
|
|
static bool url_parse_ipv6(string str, uint16_t out[8])
|
|
{
|
|
size_t i = 0;
|
|
return parse_ipv6(str, &i, out);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// URI PARSER ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool hex_to_num(char x, int *n)
|
|
{
|
|
// TODO: This is inefficient
|
|
if (x >= 'A' && x <= 'F') *n = x - 'A';
|
|
else if (x >= 'a' && x <= 'f') *n = x - 'a';
|
|
else if (x >= '0' && x <= '9') *n = x - '0';
|
|
else return false;
|
|
return true;
|
|
}
|
|
|
|
static size_t skip_any_valid_chars_not_percent_encoded(string src, size_t cur)
|
|
{
|
|
while (cur < src.size
|
|
&& src.data[cur] != '=' // Start of the value
|
|
&& src.data[cur] != '&' // End of the parameter (no value)
|
|
&& src.data[cur] != '%' // The name is percent encoded
|
|
&& src.data[cur] != '+' // Also percent encoded
|
|
&& is_print(src.data[cur])) // Query stirng contains invalid chracters
|
|
cur++;
|
|
return cur;
|
|
}
|
|
|
|
// This function is like the "parse" version but doesn't
|
|
// return the decoded string.
|
|
static bool skip_percent_encoded_substr(string src, size_t *cur_)
|
|
{
|
|
size_t cur = *cur_;
|
|
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
|
|
while (cur < src.size && (src.data[cur] == '+' || src.data[cur] == '%')) {
|
|
if (src.data[cur] == '+')
|
|
cur++;
|
|
else {
|
|
int u, v;
|
|
if (cur+2 >= src.size
|
|
|| !hex_to_num(src.data[cur+1], &u)
|
|
|| !hex_to_num(src.data[cur+2], &v))
|
|
return false; // Invalid percent encoding
|
|
cur += 3;
|
|
}
|
|
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
|
|
}
|
|
*cur_ = cur;
|
|
return true;
|
|
}
|
|
|
|
// Percent decodes the source string until a query string
|
|
// separator ('=', '&') or an invalid character. The resulting
|
|
// slice (out) may point into the source string or the
|
|
// destination buffer (dst). It only returns false if copying
|
|
// was required and the buffer was too small.
|
|
static bool parse_percent_encoded_substr(string src, string dst, size_t *cur_, string *out)
|
|
{
|
|
size_t cur = *cur_;
|
|
size_t start = cur;
|
|
|
|
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
|
|
|
|
// Use secondary pointer so the key to avoid pointing
|
|
// into the source buffer. If we found something to
|
|
// decode (eg a %xx token) we'll need to copy the key
|
|
// into the destination buffer and make this pointer
|
|
// refer to that.
|
|
*out = substr(src, start, cur);
|
|
|
|
// If the name is percent encoded, copying into
|
|
// a seprate buffer to decode it is necessary
|
|
if (cur < src.size && (src.data[cur] == '%' || src.data[cur] == '+')) {
|
|
|
|
// Copy what was already scanned into the
|
|
// output buffer.
|
|
if (out->size > dst.size)
|
|
return -1; // Output buffer is too small
|
|
memcpy(dst.data, out->data, out->size);
|
|
|
|
size_t copied = out->size;
|
|
do {
|
|
|
|
assert(cur < src.size);
|
|
|
|
if (src.data[cur] == '+') {
|
|
|
|
if (copied == dst.size) return false; // Output buffer is too small
|
|
dst.data[copied++] = ' ';
|
|
cur++;
|
|
|
|
} else {
|
|
|
|
// We know there is a '%' at this point
|
|
int u, v;
|
|
if (cur+2 >= src.size
|
|
|| !hex_to_num(src.data[cur+1], &u)
|
|
|| !hex_to_num(src.data[cur+2], &v))
|
|
return false; // Invalid percent encoding
|
|
cur += 3;
|
|
|
|
assert(u >= 0 && u < 16);
|
|
assert(v >= 0 && v < 16);
|
|
uint8_t byte = (u << 4) | v;
|
|
|
|
if (copied == dst.size) return false; // Output buffer is too small
|
|
dst.data[copied++] = (char) byte;
|
|
}
|
|
|
|
// Skip all plain characters and memcopy them at once
|
|
|
|
start = cur;
|
|
cur = skip_any_valid_chars_not_percent_encoded(src, cur);
|
|
|
|
if (copied + cur - start > dst.size) return false; // Buffer is too small
|
|
memcpy(dst.data + copied, src.data + start, cur - start);
|
|
copied += cur - start;
|
|
|
|
} while (cur < src.size && (src.data[cur] == '%' || src.data[cur] == '+'));
|
|
|
|
out->data = dst.data;
|
|
out->size = copied;
|
|
}
|
|
|
|
*cur_ = cur;
|
|
return true;
|
|
}
|
|
|
|
bool get_query_string_param(string str, string key, string dst, string *out)
|
|
{
|
|
assert(key.size);
|
|
|
|
size_t cur = 0;
|
|
if (cur < str.size && str.data[cur] == '?')
|
|
cur++;
|
|
|
|
while (cur < str.size) {
|
|
|
|
string key2;
|
|
if (!parse_percent_encoded_substr(str, dst, &cur, &key2))
|
|
return false; // Buffer too small
|
|
|
|
if (cur < str.size && !is_print(str.data[cur]))
|
|
return false; // Invalid query string
|
|
|
|
if (key2.size == key.size && !memcmp(key2.data, key.data, key2.size)) {
|
|
if (cur < str.size && str.data[cur] == '=') {
|
|
cur++; // =
|
|
if (!parse_percent_encoded_substr(str, dst, &cur, out))
|
|
return false;
|
|
} else {
|
|
out->data = "";
|
|
out->size = 0;
|
|
}
|
|
return true;
|
|
} else {
|
|
if (cur < str.size && str.data[cur] == '=') {
|
|
cur++; // =
|
|
if (!skip_percent_encoded_substr(str, &cur))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cur < str.size && str.data[cur] != '&')
|
|
return false;
|
|
cur++; // &
|
|
}
|
|
return false;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// COOKIE PARSER ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool is_cookie_name(char c)
|
|
{
|
|
return (c >= 'a' && c <= 'z')
|
|
|| (c >= 'A' && c <= 'Z')
|
|
|| (c >= '0' && c <= '9')
|
|
|| c == '_' || c == '-';
|
|
}
|
|
|
|
static bool is_cookie_value(char c)
|
|
{
|
|
return (c >= 'a' && c <= 'z')
|
|
|| (c >= 'A' && c <= 'Z')
|
|
|| (c >= '0' && c <= '9')
|
|
|| c == '_' || c == '-';
|
|
}
|
|
|
|
bool get_cookie(Request *request, string name, string *out)
|
|
{
|
|
string value;
|
|
if (!find_header(request, LIT("Cookie"), &value))
|
|
return false;
|
|
|
|
char *src = value.data;
|
|
size_t len = value.size;
|
|
size_t cur = 0;
|
|
while (cur < len) {
|
|
|
|
size_t start;
|
|
|
|
while (cur < len && is_space(src[cur]))
|
|
cur++;
|
|
|
|
if (cur == len)
|
|
break;
|
|
|
|
if (!is_cookie_name(src[cur]))
|
|
return false;
|
|
|
|
start = cur;
|
|
do
|
|
cur++;
|
|
while (cur < len && is_cookie_name(src[cur]));
|
|
string cookie_name = substr(value, start, cur);
|
|
|
|
while (cur < len && is_space(src[cur]))
|
|
cur++;
|
|
|
|
if (cur == len || src[cur] != '=')
|
|
return false;
|
|
cur++;
|
|
|
|
while (cur < len && is_space(src[cur]))
|
|
cur++;
|
|
|
|
if (!is_cookie_value(src[cur]))
|
|
return false;
|
|
|
|
start = cur;
|
|
do
|
|
cur++;
|
|
while (cur < len && is_cookie_value(src[cur]));
|
|
string cookie_value = substr(value, start, cur);
|
|
|
|
while (cur < len && is_space(src[cur]))
|
|
cur++;
|
|
|
|
if (cur < len) {
|
|
if (src[cur] != ';')
|
|
return false;
|
|
cur++;
|
|
}
|
|
|
|
if (streq(cookie_name, name)) {
|
|
*out = cookie_value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// CERTIFICATE AND PRIVATE KEY PARSING (Adapted from BearSSL) ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
/// Copyright (c) 2016 Thomas Pornin <pornin@bolet.org> ///
|
|
/// ///
|
|
/// 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. ///
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#if HTTPS
|
|
BearSSLErrorInfo bearssl_error_table[] = {
|
|
{ BR_ERR_BAD_PARAM, LIT("BR_ERR_BAD_PARAM"), LIT("Caller-provided parameter is incorrect.") },
|
|
{ BR_ERR_BAD_STATE, LIT("BR_ERR_BAD_STATE"), LIT("Operation requested by the caller cannot be applied with the current context state (e.g. reading data while outgoing data is waiting to be sent).") },
|
|
{ BR_ERR_UNSUPPORTED_VERSION, LIT("BR_ERR_UNSUPPORTED_VERSION"), LIT("Incoming protocol or record version is unsupported.") },
|
|
{ BR_ERR_BAD_VERSION, LIT("BR_ERR_BAD_VERSION"), LIT("Incoming record version does not match the expected version.") },
|
|
{ BR_ERR_BAD_LENGTH, LIT("BR_ERR_BAD_LENGTH"), LIT("Incoming record length is invalid.") },
|
|
{ BR_ERR_TOO_LARGE, LIT("BR_ERR_TOO_LARGE"), LIT("Incoming record is too large to be processed, or buffer is too small for the handshake message to send.") },
|
|
{ BR_ERR_BAD_MAC, LIT("BR_ERR_BAD_MAC"), LIT("Decryption found an invalid padding, or the record MAC is not correct.") },
|
|
{ BR_ERR_NO_RANDOM, LIT("BR_ERR_NO_RANDOM"), LIT("No initial entropy was provided, and none can be obtained from the OS.") },
|
|
{ BR_ERR_UNKNOWN_TYPE, LIT("BR_ERR_UNKNOWN_TYPE"), LIT("Incoming record type is unknown.") },
|
|
{ BR_ERR_UNEXPECTED, LIT("BR_ERR_UNEXPECTED"), LIT("Incoming record or message has wrong type with regards to the current engine state.") },
|
|
{ BR_ERR_BAD_CCS, LIT("BR_ERR_BAD_CCS"), LIT("ChangeCipherSpec message from the peer has invalid contents.") },
|
|
{ BR_ERR_BAD_ALERT, LIT("BR_ERR_BAD_ALERT"), LIT("Alert message from the peer has invalid contents (odd length).") },
|
|
{ BR_ERR_BAD_HANDSHAKE, LIT("BR_ERR_BAD_HANDSHAKE"), LIT("Incoming handshake message decoding failed.") },
|
|
{ BR_ERR_OVERSIZED_ID, LIT("BR_ERR_OVERSIZED_ID"), LIT("ServerHello contains a session ID which is larger than 32 bytes.") },
|
|
{ BR_ERR_BAD_CIPHER_SUITE, LIT("BR_ERR_BAD_CIPHER_SUITE"), LIT("Server wants to use a cipher suite that we did not claim to support. This is also reported if we tried to advertise a cipher suite that we do not support.") },
|
|
{ BR_ERR_BAD_COMPRESSION, LIT("BR_ERR_BAD_COMPRESSION"), LIT("Server wants to use a compression that we did not claim to support.") },
|
|
{ BR_ERR_BAD_FRAGLEN, LIT("BR_ERR_BAD_FRAGLEN"), LIT("Server's max fragment length does not match client's.") },
|
|
{ BR_ERR_BAD_SECRENEG, LIT("BR_ERR_BAD_SECRENEG"), LIT("Secure renegotiation failed.") },
|
|
{ BR_ERR_EXTRA_EXTENSION, LIT("BR_ERR_EXTRA_EXTENSION"), LIT("Server sent an extension type that we did not announce, or used the same extension type several times in a single ServerHello.") },
|
|
{ BR_ERR_BAD_SNI, LIT("BR_ERR_BAD_SNI"), LIT("Invalid Server Name Indication contents (when used by the server, this extension shall be empty).") },
|
|
{ BR_ERR_BAD_HELLO_DONE, LIT("BR_ERR_BAD_HELLO_DONE"), LIT("Invalid ServerHelloDone from the server (length is not 0).") },
|
|
{ BR_ERR_LIMIT_EXCEEDED, LIT("BR_ERR_LIMIT_EXCEEDED"), LIT("Internal limit exceeded (e.g. server's public key is too large).") },
|
|
{ BR_ERR_BAD_FINISHED, LIT("BR_ERR_BAD_FINISHED"), LIT("Finished message from peer does not match the expected value.") },
|
|
{ BR_ERR_RESUME_MISMATCH, LIT("BR_ERR_RESUME_MISMATCH"), LIT("Session resumption attempt with distinct version or cipher suite.") },
|
|
{ BR_ERR_INVALID_ALGORITHM, LIT("BR_ERR_INVALID_ALGORITHM"), LIT("Unsupported or invalid algorithm (ECDHE curve, signature algorithm, hash function).") },
|
|
{ BR_ERR_BAD_SIGNATURE, LIT("BR_ERR_BAD_SIGNATURE"), LIT("Invalid signature in ServerKeyExchange or CertificateVerify message.") },
|
|
{ BR_ERR_WRONG_KEY_USAGE, LIT("BR_ERR_WRONG_KEY_USAGE"), LIT("Peer's public key does not have the proper type or is not allowed for the requested operation.") },
|
|
{ BR_ERR_NO_CLIENT_AUTH, LIT("BR_ERR_NO_CLIENT_AUTH"), LIT("Client did not send a certificate upon request, or the client certificate could not be validated.") },
|
|
{ BR_ERR_IO, LIT("BR_ERR_IO"), LIT("I/O error or premature close on transport stream.") },
|
|
{ BR_ERR_X509_INVALID_VALUE, LIT("BR_ERR_X509_INVALID_VALUE"), LIT("Invalid value in an ASN.1 structure.") },
|
|
{ BR_ERR_X509_TRUNCATED, LIT("BR_ERR_X509_TRUNCATED"), LIT("Truncated certificate or other ASN.1 object.") },
|
|
{ BR_ERR_X509_EMPTY_CHAIN, LIT("BR_ERR_X509_EMPTY_CHAIN"), LIT("Empty certificate chain (no certificate at all).") },
|
|
{ BR_ERR_X509_INNER_TRUNC, LIT("BR_ERR_X509_INNER_TRUNC"), LIT("Decoding error: inner element extends beyond outer element size.") },
|
|
{ BR_ERR_X509_BAD_TAG_CLASS, LIT("BR_ERR_X509_BAD_TAG_CLASS"), LIT("Decoding error: unsupported tag class (application or private).") },
|
|
{ BR_ERR_X509_BAD_TAG_VALUE, LIT("BR_ERR_X509_BAD_TAG_VALUE"), LIT("Decoding error: unsupported tag value.") },
|
|
{ BR_ERR_X509_INDEFINITE_LENGTH, LIT("BR_ERR_X509_INDEFINITE_LENGTH"), LIT("Decoding error: indefinite length.") },
|
|
{ BR_ERR_X509_EXTRA_ELEMENT, LIT("BR_ERR_X509_EXTRA_ELEMENT"), LIT("Decoding error: extraneous element.") },
|
|
{ BR_ERR_X509_UNEXPECTED, LIT("BR_ERR_X509_UNEXPECTED"), LIT("Decoding error: unexpected element.") },
|
|
{ BR_ERR_X509_NOT_CONSTRUCTED, LIT("BR_ERR_X509_NOT_CONSTRUCTED"), LIT("Decoding error: expected constructed element, but is primitive.") },
|
|
{ BR_ERR_X509_NOT_PRIMITIVE, LIT("BR_ERR_X509_NOT_PRIMITIVE"), LIT("Decoding error: expected primitive element, but is constructed.") },
|
|
{ BR_ERR_X509_PARTIAL_BYTE, LIT("BR_ERR_X509_PARTIAL_BYTE"), LIT("Decoding error: BIT STRING length is not multiple of 8.") },
|
|
{ BR_ERR_X509_BAD_BOOLEAN, LIT("BR_ERR_X509_BAD_BOOLEAN"), LIT("Decoding error: BOOLEAN value has invalid length.") },
|
|
{ BR_ERR_X509_OVERFLOW, LIT("BR_ERR_X509_OVERFLOW"), LIT("Decoding error: value is off-limits.") },
|
|
{ BR_ERR_X509_BAD_DN, LIT("BR_ERR_X509_BAD_DN"), LIT("Invalid distinguished name.") },
|
|
{ BR_ERR_X509_BAD_TIME, LIT("BR_ERR_X509_BAD_TIME"), LIT("Invalid date/time representation.") },
|
|
{ BR_ERR_X509_UNSUPPORTED, LIT("BR_ERR_X509_UNSUPPORTED"), LIT("Certificate contains unsupported features that cannot be ignored.") },
|
|
{ BR_ERR_X509_LIMIT_EXCEEDED, LIT("BR_ERR_X509_LIMIT_EXCEEDED"), LIT("Key or signature size exceeds internal limits.") },
|
|
{ BR_ERR_X509_WRONG_KEY_TYPE, LIT("BR_ERR_X509_WRONG_KEY_TYPE"), LIT("Key type does not match that which was expected.") },
|
|
{ BR_ERR_X509_BAD_SIGNATURE, LIT("BR_ERR_X509_BAD_SIGNATURE"), LIT("Signature is invalid.") },
|
|
{ BR_ERR_X509_TIME_UNKNOWN, LIT("BR_ERR_X509_TIME_UNKNOWN"), LIT("Validation time is unknown.") },
|
|
{ BR_ERR_X509_EXPIRED, LIT("BR_ERR_X509_EXPIRED"), LIT("Certificate is expired or not yet valid.") },
|
|
{ BR_ERR_X509_DN_MISMATCH, LIT("BR_ERR_X509_DN_MISMATCH"), LIT("Issuer/Subject DN mismatch in the chain.") },
|
|
{ BR_ERR_X509_BAD_SERVER_NAME, LIT("BR_ERR_X509_BAD_SERVER_NAME"), LIT("Expected server name was not found in the chain.") },
|
|
{ BR_ERR_X509_CRITICAL_EXTENSION, LIT("BR_ERR_X509_CRITICAL_EXTENSION"), LIT("Unknown critical extension in certificate.") },
|
|
{ BR_ERR_X509_NOT_CA, LIT("BR_ERR_X509_NOT_CA"), LIT("Not a CA, or path length constraint violation.") },
|
|
{ BR_ERR_X509_FORBIDDEN_KEY_USAGE, LIT("BR_ERR_X509_FORBIDDEN_KEY_USAGE"), LIT("Key Usage extension prohibits intended usage.") },
|
|
{ BR_ERR_X509_WEAK_PUBLIC_KEY, LIT("BR_ERR_X509_WEAK_PUBLIC_KEY"), LIT("Public key found in certificate is too small.") },
|
|
{ BR_ERR_X509_NOT_TRUSTED, LIT("BR_ERR_X509_NOT_TRUSTED"), LIT("Chain could not be linked to a trust anchor.") },
|
|
};
|
|
|
|
static BearSSLErrorInfo get_bearssl_error_info(int code)
|
|
{
|
|
for (int i = 0; i < COUNTOF(bearssl_error_table); i++)
|
|
if (bearssl_error_table[i].code == code)
|
|
return bearssl_error_table[i];
|
|
BearSSLErrorInfo fallback;
|
|
fallback.code = code;
|
|
fallback.name = LIT("Unknown");
|
|
fallback.comment = LIT(":/");
|
|
return fallback;
|
|
}
|
|
|
|
typedef struct {
|
|
string name;
|
|
string content;
|
|
} PemObject;
|
|
|
|
typedef struct {
|
|
|
|
bool failed;
|
|
|
|
char *buffer;
|
|
size_t buffer_count;
|
|
size_t buffer_capacity;
|
|
|
|
} PemDecodeContext;
|
|
|
|
static int is_ign(int c)
|
|
{
|
|
if (c == 0) {
|
|
return 0;
|
|
}
|
|
if (c <= 32 || c == '-' || c == '_' || c == '.'
|
|
|| c == '/' || c == '+' || c == ':')
|
|
{
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Get next non-ignored character, normalised:
|
|
// ASCII letters are converted to lowercase
|
|
// control characters, space, '-', '_', '.', '/', '+' and ':' are ignored
|
|
// A terminating zero is returned as 0.
|
|
static char next_char(string *s)
|
|
{
|
|
size_t i = 0;
|
|
while (i < s->size && is_ign(s->data[i]))
|
|
i++;
|
|
|
|
char c;
|
|
if (i == s->size)
|
|
c = '\0';
|
|
else {
|
|
c = s->data[i++];
|
|
assert(c != '\0');
|
|
}
|
|
|
|
s->data += i;
|
|
s->size -= i;
|
|
return to_lower(c);
|
|
}
|
|
|
|
static bool eqstr__(string a, string b)
|
|
{
|
|
for (;;) {
|
|
char c1 = next_char(&a);
|
|
char c2 = next_char(&b);
|
|
if (c1 != c2) return false;
|
|
if (c1 == 0) return true;
|
|
}
|
|
}
|
|
|
|
static void append_bytes(void *userptr, const void *str, size_t len)
|
|
{
|
|
PemDecodeContext *context = userptr;
|
|
|
|
if (context->failed)
|
|
return;
|
|
|
|
if (context->buffer_capacity - context->buffer_count < len) {
|
|
|
|
size_t newcap = MAX(2 * context->buffer_capacity, context->buffer_count + len);
|
|
void *newstr = mymalloc(newcap);
|
|
if (newstr == NULL) {
|
|
context->failed = true;
|
|
return;
|
|
}
|
|
|
|
if (context->buffer) {
|
|
memcpy(newstr, context->buffer, context->buffer_count);
|
|
myfree(context->buffer, context->buffer_capacity);
|
|
}
|
|
context->buffer = newstr;
|
|
context->buffer_capacity = newcap;
|
|
}
|
|
|
|
memcpy(context->buffer + context->buffer_count, str, len);
|
|
context->buffer_count += len;
|
|
}
|
|
|
|
typedef struct {
|
|
PemObject *items;
|
|
int count;
|
|
int capacity;
|
|
} PemArray;
|
|
|
|
static bool append_pem(PemArray *arr, PemObject obj)
|
|
{
|
|
if (arr->count == arr->capacity) {
|
|
int newcap = MAX(2 * arr->capacity, 4);
|
|
PemObject *newitems = mymalloc(newcap * sizeof(PemObject));
|
|
if (newitems == NULL)
|
|
return false;
|
|
if (arr->count)
|
|
memcpy(arr->items, newitems, arr->count * sizeof(PemObject));
|
|
myfree(arr->items, arr->capacity * sizeof(PemObject));
|
|
arr->items = newitems;
|
|
arr->capacity = newcap;
|
|
}
|
|
|
|
arr->items[arr->count++] = obj;
|
|
return true;
|
|
}
|
|
|
|
static void free_pem_array(PemArray *arr)
|
|
{
|
|
for (int i = 0; i < arr->count; i++) {
|
|
|
|
string name = arr->items[i].name;
|
|
myfree(name.data, name.size);
|
|
|
|
string content = arr->items[i].content;
|
|
if (content.data)
|
|
myfree(content.data, content.size);
|
|
}
|
|
myfree(arr->items, arr->capacity * sizeof(PemObject));
|
|
}
|
|
|
|
static bool decode_pem(string src, PemArray *array)
|
|
{
|
|
br_pem_decoder_context context;
|
|
br_pem_decoder_init(&context);
|
|
|
|
PemDecodeContext context2;
|
|
context2.failed = false;
|
|
context2.buffer = NULL;
|
|
context2.buffer_count = 0;
|
|
context2.buffer_capacity = 0;
|
|
|
|
array->items = NULL;
|
|
array->count = 0;
|
|
array->capacity = 0;
|
|
|
|
PemObject po;
|
|
po.name = NULLSTR;
|
|
po.content = NULLSTR;
|
|
|
|
bool inside_object = false;
|
|
bool extra_newline = true;
|
|
while (src.size > 0) {
|
|
|
|
size_t n = br_pem_decoder_push(&context, src.data, src.size);
|
|
src.data += n;
|
|
src.size -= n;
|
|
|
|
switch (br_pem_decoder_event(&context)) {
|
|
case BR_PEM_BEGIN_OBJ:
|
|
{
|
|
const char *name = br_pem_decoder_name(&context);
|
|
size_t name_len = strlen(name);
|
|
|
|
po.name.data = mymalloc(name_len);
|
|
po.name.size = name_len;
|
|
|
|
if (po.name.data == NULL) {
|
|
myfree(context2.buffer, context2.buffer_capacity);
|
|
free_pem_array(array);
|
|
return false;
|
|
}
|
|
memcpy(po.name.data, name, name_len);
|
|
|
|
br_pem_decoder_setdest(&context, append_bytes, &context2);
|
|
inside_object = true;
|
|
}
|
|
break;
|
|
|
|
case BR_PEM_END_OBJ:
|
|
if (inside_object) {
|
|
|
|
void *copy = mymalloc(context2.buffer_count);
|
|
if (copy == NULL) {
|
|
myfree(po.name.data, po.name.size);
|
|
myfree(context2.buffer, context2.buffer_capacity);
|
|
free_pem_array(array);
|
|
return false;
|
|
}
|
|
memcpy(copy, context2.buffer, context2.buffer_count);
|
|
|
|
po.content.data = copy;
|
|
po.content.size = context2.buffer_count;
|
|
|
|
if (!append_pem(array, po)) {
|
|
myfree(po.name.data, po.name.size);
|
|
myfree(context2.buffer, context2.buffer_capacity);
|
|
free_pem_array(array);
|
|
return false;
|
|
}
|
|
|
|
po.name = NULLSTR;
|
|
po.content = NULLSTR;
|
|
|
|
context2.buffer_count = 0;
|
|
inside_object = false;
|
|
}
|
|
break;
|
|
|
|
case BR_PEM_ERROR:
|
|
myfree(po.name.data, po.name.size);
|
|
myfree(context2.buffer, context2.buffer_capacity);
|
|
free_pem_array(array);
|
|
log_data(LIT("Invalid PEM"));
|
|
return false;
|
|
}
|
|
|
|
if (src.size == 0 && extra_newline) {
|
|
src.data = "\n";
|
|
src.size = 1;
|
|
extra_newline = false;
|
|
}
|
|
}
|
|
|
|
if (context2.buffer)
|
|
myfree(context2.buffer, context2.buffer_capacity);
|
|
|
|
if (context2.failed) {
|
|
myfree(po.name.data, po.name.size);
|
|
free_pem_array(array);
|
|
return false;
|
|
}
|
|
if (inside_object) {
|
|
myfree(po.name.data, po.name.size);
|
|
free_pem_array(array);
|
|
log_data(LIT("Unfinished PEM"));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int looks_like_DER(string content)
|
|
{
|
|
int fb;
|
|
size_t dlen;
|
|
|
|
if (content.size < 2) {
|
|
return 0;
|
|
}
|
|
if (*content.data ++ != 0x30) {
|
|
return 0;
|
|
}
|
|
fb = *content.data ++;
|
|
content.size -= 2;
|
|
if (fb < 0x80) {
|
|
return (size_t)fb == content.size;
|
|
} else if (fb == 0x80) {
|
|
return 0;
|
|
} else {
|
|
fb -= 0x80;
|
|
if (content.size < (size_t)fb + 2) {
|
|
return 0;
|
|
}
|
|
content.size -= (size_t)fb;
|
|
dlen = 0;
|
|
while (fb -- > 0) {
|
|
if (dlen > (content.size >> 8)) {
|
|
return 0;
|
|
}
|
|
dlen = (dlen << 8) + (size_t)*content.data ++;
|
|
}
|
|
return dlen == content.size;
|
|
}
|
|
}
|
|
|
|
static bool decode_key(string src, PrivateKey *pkey)
|
|
{
|
|
br_skey_decoder_context context;
|
|
br_skey_decoder_init(&context);
|
|
br_skey_decoder_push(&context, src.data, src.size);
|
|
|
|
int err = br_skey_decoder_last_error(&context);
|
|
if (err) {
|
|
BearSSLErrorInfo error_info = get_bearssl_error_info(err);
|
|
log_format("Error decoding key: %.*s: (code=%d, %.*s)\n",
|
|
(int) error_info.name.size, error_info.name.data, err,
|
|
(int) error_info.comment.size, error_info.comment.data);
|
|
return false;
|
|
}
|
|
|
|
switch (br_skey_decoder_key_type(&context)) {
|
|
|
|
const br_rsa_private_key *rsa_key;
|
|
const br_ec_private_key *ec_key;
|
|
|
|
case BR_KEYTYPE_RSA:
|
|
{
|
|
rsa_key = br_skey_decoder_get_rsa(&context);
|
|
|
|
unsigned char *mem = mymalloc(rsa_key->plen + rsa_key->qlen + rsa_key->dplen + rsa_key->dqlen + rsa_key->iqlen);
|
|
if (mem == NULL) return false;
|
|
|
|
pkey->type = BR_KEYTYPE_RSA;
|
|
pkey->rsa.n_bitlen = rsa_key->n_bitlen;
|
|
|
|
pkey->rsa.p = mem;
|
|
pkey->rsa.q = mem + rsa_key->plen;
|
|
pkey->rsa.dp = mem + rsa_key->plen + rsa_key->qlen;
|
|
pkey->rsa.dq = mem + rsa_key->plen + rsa_key->qlen + rsa_key->dplen;
|
|
pkey->rsa.iq = mem + rsa_key->plen + rsa_key->qlen + rsa_key->dplen + rsa_key->dqlen;
|
|
|
|
memcpy(pkey->rsa.p, rsa_key->p, rsa_key->plen);
|
|
memcpy(pkey->rsa.q, rsa_key->q, rsa_key->qlen);
|
|
memcpy(pkey->rsa.dp, rsa_key->dp, rsa_key->dplen);
|
|
memcpy(pkey->rsa.dq, rsa_key->dq, rsa_key->dqlen);
|
|
memcpy(pkey->rsa.iq, rsa_key->iq, rsa_key->iqlen);
|
|
|
|
pkey->rsa.plen = rsa_key->plen;
|
|
pkey->rsa.qlen = rsa_key->qlen;
|
|
pkey->rsa.dplen = rsa_key->dplen;
|
|
pkey->rsa.dqlen = rsa_key->dqlen;
|
|
pkey->rsa.iqlen = rsa_key->iqlen;
|
|
}
|
|
break;
|
|
|
|
case BR_KEYTYPE_EC:
|
|
{
|
|
ec_key = br_skey_decoder_get_ec(&context);
|
|
pkey->type = BR_KEYTYPE_EC;
|
|
pkey->ec.curve = ec_key->curve;
|
|
pkey->ec.x = mymalloc(ec_key->xlen);
|
|
if (pkey->ec.x == NULL)
|
|
return false;
|
|
memcpy(pkey->ec.x, ec_key->x, ec_key->xlen);
|
|
pkey->ec.xlen = ec_key->xlen;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
log_format("Unknown key type: %d\n", br_skey_decoder_key_type(&context));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool load_private_key_from_file(string file, PrivateKey *pkey)
|
|
{
|
|
string file_contents;
|
|
if (!load_file_contents(file, &file_contents))
|
|
return false;
|
|
|
|
DEBUG("loading key: file contents loaded\n");
|
|
|
|
bool ok;
|
|
if (looks_like_DER(file_contents)) {
|
|
DEBUG("loading key: detected DER file\n");
|
|
ok = decode_key(file_contents, pkey);
|
|
} else {
|
|
|
|
DEBUG("loading key: detected PEM file\n");
|
|
|
|
PemArray pem_array;
|
|
if (!decode_pem(file_contents, &pem_array)) {
|
|
myfree(file_contents.data, file_contents.size);
|
|
return false;
|
|
}
|
|
|
|
bool found = false;
|
|
bool decoded = false;
|
|
for (int i = 0; i < pem_array.count; i++)
|
|
if (eqstr__(pem_array.items[i].name, LIT("RSA PRIVATE KEY"))
|
|
|| eqstr__(pem_array.items[i].name, LIT("EC PRIVATE KEY"))
|
|
|| eqstr__(pem_array.items[i].name, LIT("PRIVATE KEY"))) {
|
|
|
|
DEBUG("loading key: found key in PEM file\n");
|
|
|
|
if (decode_key(pem_array.items[i].content, pkey))
|
|
decoded = true;
|
|
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
ok = false;
|
|
if (!found)
|
|
log_data(LIT("Missing private key in file\n"));
|
|
else {
|
|
if (!decoded)
|
|
log_data(LIT("Couldn't decode key\n"));
|
|
else
|
|
ok = true;
|
|
}
|
|
free_pem_array(&pem_array);
|
|
}
|
|
|
|
myfree(file_contents.data, file_contents.size);
|
|
return ok;
|
|
}
|
|
|
|
static void free_private_key(PrivateKey *pkey)
|
|
{
|
|
switch (pkey->type) {
|
|
|
|
case BR_KEYTYPE_RSA:
|
|
myfree(pkey->rsa.p, pkey->rsa.plen + pkey->rsa.qlen + pkey->rsa.dplen + pkey->rsa.dqlen + pkey->rsa.iqlen);
|
|
break;
|
|
|
|
case BR_KEYTYPE_EC:
|
|
myfree(pkey->ec.x, pkey->ec.xlen);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool append_cert(CertArray *arr, br_x509_certificate cert)
|
|
{
|
|
if (arr->count == arr->capacity) {
|
|
int newcap = MAX(2 * arr->capacity, 4);
|
|
br_x509_certificate *newitems = mymalloc(newcap * sizeof(br_x509_certificate));
|
|
if (newitems == NULL)
|
|
return false;
|
|
if (arr->count)
|
|
memcpy(arr->items, newitems, arr->count * sizeof(br_x509_certificate));
|
|
myfree(arr->items, arr->capacity * sizeof(br_x509_certificate));
|
|
arr->items = newitems;
|
|
arr->capacity = newcap;
|
|
}
|
|
|
|
arr->items[arr->count++] = cert;
|
|
return true;
|
|
}
|
|
|
|
static bool load_certs_from_file(string file, CertArray *array)
|
|
{
|
|
string file_contents;
|
|
if (!load_file_contents(file, &file_contents))
|
|
return false;
|
|
|
|
DEBUG("loading certs: file contents loaded\n");
|
|
|
|
array->items = NULL;
|
|
array->count = 0;
|
|
array->capacity = 0;
|
|
|
|
if (looks_like_DER(file_contents)) {
|
|
|
|
DEBUG("loading certs: detected DER file\n");
|
|
|
|
br_x509_certificate xc = {
|
|
(unsigned char*) file_contents.data,
|
|
file_contents.size,
|
|
};
|
|
if (!append_cert(array, xc)) {
|
|
myfree(file_contents.data, file_contents.size);
|
|
return false;
|
|
}
|
|
|
|
DEBUG("loading certs: DER file parsed\n");
|
|
|
|
} else {
|
|
|
|
DEBUG("loading certs: detected PEM file\n");
|
|
|
|
PemArray pem_array;
|
|
if (!decode_pem(file_contents, &pem_array)) {
|
|
myfree(file_contents.data, file_contents.size);
|
|
return false;
|
|
}
|
|
|
|
DEBUG("loading certs: PEM file parsed (%d entries)\n", pem_array.count);
|
|
|
|
for (int i = 0; i < pem_array.count; i++) {
|
|
|
|
PemObject po = pem_array.items[i];
|
|
|
|
if (eqstr__(po.name, LIT("CERTIFICATE")) || eqstr__(po.name, LIT("X509 CERTIFICATE"))) {
|
|
|
|
DEBUG("loading certs: found certificate in PEM file\n");
|
|
|
|
br_x509_certificate xc = { (unsigned char*) po.content.data, po.content.size };
|
|
|
|
if (!append_cert(array, xc)) {
|
|
free_pem_array(&pem_array);
|
|
free_certs(array);
|
|
myfree(file_contents.data, file_contents.size);
|
|
return false;
|
|
}
|
|
|
|
pem_array.items[i].content = NULLSTR;
|
|
} else {
|
|
DEBUG("loading certs: ignoring entry [%.*s] in PEM file\n", (int) po.name.size, po.name.data);
|
|
}
|
|
}
|
|
|
|
DEBUG("loading certs: finished loading certificates from PEM file\n");
|
|
|
|
if (array->count == 0) {
|
|
free_pem_array(&pem_array);
|
|
free_certs(array);
|
|
myfree(file_contents.data, file_contents.size);
|
|
log_data(LIT("No certificates in file\n"));
|
|
return false;
|
|
}
|
|
|
|
free_pem_array(&pem_array);
|
|
myfree(file_contents.data, file_contents.size);
|
|
}
|
|
|
|
DEBUG("loading certs: certificate loaded\n");
|
|
return true;
|
|
}
|
|
|
|
static void free_certs(CertArray *array)
|
|
{
|
|
for (int i = 0; i < array->count; i++) {
|
|
br_x509_certificate item = array->items[i];
|
|
myfree(item.data, item.data_len);
|
|
}
|
|
myfree(array->items, array->capacity * sizeof(br_x509_certificate));
|
|
}
|
|
#endif /* HTTPS */
|