Hardened HTTPS server example with SSL/TLS security best practices.
#ifdef __windows__
#define realpath(rel, abs) _fullpath(abs, rel, MAX_PATH)
#endif
#define MAX_HTTP_REQUEST_SIZE_LIMIT 1048576
#define CONNECTION_TIMEOUT_SECONDS 10
#define SECURITY_HEADERS \
"Strict-Transport-Security: max-age=31536000\r\n" \
"X-Content-Type-Options: nosniff\r\n" \
"X-Frame-Options: DENY\r\n" \
"Content-Security-Policy: default-src 'self'\r\n" \
"Referrer-Policy: strict-origin-when-cross-origin\r\n" \
"Permissions-Policy: geolocation=(), camera=(), microphone=()\r\n"
fprintf(stderr,
" -p 'port' : set the https server port\n"
" -k 'key file' : SSL key file path\n"
" -c 'cert file' : SSL certificate file path\n"
" -A 'CA file' : optional, CA certificate file for chain verification\n"
" -e : optional, enable SSL peer certificate verification\n"
" -a 'address name/ip' : optional, specify where to bind interface\n"
" -i 'ipmode' : optional, force 'ipv4' or 'ipv6', default supports both\n"
" -s 'size' : optional, maximum http request size (default: %d, max: %d)\n"
" -n 'count' : optional, exit after count connections (0 = unlimited, default)\n"
" -d 'html root' : optional, specify a different http root dir (default: ./DATAS/)\n"
" -v : version\n"
" -h : help\n"
" -V 'log level' : optional, set the log level (default: LOG_ERR)\n",
}
void process_args(
int argc_nb,
char** argv_ptr,
char** addr_ptr,
char** port_ptr,
char** key_ptr,
char** cert_ptr,
char** ca_file_ptr,
int* ssl_verify_ptr,
LIST* routes_ptr,
int* ip_version_ptr,
int* max_http_request_size_ptr,
char** root_dir_ptr) {
if (argc_nb == 1) {
fprintf(stderr, "No arguments given, help:\n");
exit(1);
}
while ((
getoptret = getopt(argc_nb, argv_ptr,
"hven:s:V:p:i:a:r:k:c:s:d:A:")) != EOF) {
case 'n':
break;
case 'i':
if (!strcmp("v4", optarg)) {
} else if (!strcmp("v6", optarg)) {
} else {
}
break;
case 'v':
fprintf(stderr, "Date de compilation : %s a %s.\n", __DATE__, __TIME__);
exit(1);
case 'V':
if (!strncmp("LOG_NULL", optarg, 8)) {
} else {
if (!strncmp("LOG_NOTICE", optarg, 10)) {
} else {
if (!strncmp("LOG_INFO", optarg, 8)) {
} else {
if (!strncmp("LOG_ERR", optarg, 7)) {
} else {
if (!strncmp("LOG_DEBUG", optarg, 9)) {
} else {
fprintf(stderr, "%s n'est pas un niveau de log valide.\n", optarg);
exit(-1);
}
}
}
}
}
break;
case 'e':
(*ssl_verify_ptr) = 1;
break;
case 'p':
(*port_ptr) = strdup(optarg);
break;
case 'r':
list_push(routes_ptr, strdup(optarg), &free);
break;
case 'a':
(*addr_ptr) = strdup(optarg);
break;
case 'k':
(*key_ptr) = strdup(optarg);
break;
case 'c':
(*cert_ptr) = strdup(optarg);
break;
case 'A':
(*ca_file_ptr) = strdup(optarg);
break;
case 's': {
int val = atoi(optarg);
exit(1);
}
(*max_http_request_size_ptr) = val;
break;
}
case 'd':
(*root_dir_ptr) = strdup(optarg);
break;
default:
case '?': {
if (optopt == 'd') {
fprintf(stderr, "\n Missing html root directory\n");
}
if (optopt == 's') {
fprintf(stderr, "\n Missing max http size string\n");
}
if (optopt == 'k') {
fprintf(stderr, "\n Missing key file string\n");
}
if (optopt == 'c') {
fprintf(stderr, "\n Missing certificate file string\n");
}
if (optopt == 'A') {
fprintf(stderr, "\n Missing CA file string\n");
}
if (optopt == 'r') {
fprintf(stderr, "\n Missing route string\n");
}
if (optopt == 'a') {
fprintf(stderr, "\n Missing binding host/addr string\n");
}
if (optopt == 'i') {
fprintf(stderr, "\n Missing ip version (v4 or v6) string \n");
} else if (optopt == 'V') {
fprintf(stderr, "\n Missing log level string\n");
} else if (optopt == 'p') {
fprintf(stderr, "\n Missing port\n");
} else if (optopt != 's') {
fprintf(stderr, "\n Unknow missing option %c\n", optopt);
}
exit(1);
}
case 'h': {
exit(1);
}
}
}
}
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
if (!SSL_CTX_set_min_proto_version(ssl_netw->
ctx, TLS1_2_VERSION)) {
return FALSE;
}
#else
SSL_CTX_set_options(ssl_netw->
ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1);
#endif
SSL_CTX_set_options(ssl_netw->
ctx,
SSL_OP_NO_COMPRESSION |
SSL_OP_CIPHER_SERVER_PREFERENCE |
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION |
SSL_OP_NO_TICKET |
SSL_OP_SINGLE_DH_USE |
SSL_OP_SINGLE_ECDH_USE);
const char* cipher_list = "ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:!aNULL:!eNULL:!MD5:!DSS:!RC4:!3DES:!SEED:!IDEA:!CAMELLIA";
if (!SSL_CTX_set_cipher_list(ssl_netw->
ctx, cipher_list)) {
return FALSE;
}
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
SSL_CTX_set_ciphersuites(ssl_netw->
ctx,
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256");
#endif
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
if (!SSL_CTX_set1_groups_list(ssl_netw->
ctx,
"X25519:P-256:P-384")) {
return FALSE;
}
#elif OPENSSL_VERSION_NUMBER >= 0x10002000L
SSL_CTX_set_ecdh_auto(ssl_netw->
ctx, 1);
#endif
SSL_CTX_set_session_cache_mode(ssl_netw->
ctx, SSL_SESS_CACHE_SERVER);
SSL_CTX_set_timeout(ssl_netw->
ctx, 300);
n_log(
LOG_INFO,
"SSL context hardened: TLS 1.2+, strong ciphers, no compression");
return TRUE;
}
#ifndef __windows__
struct timeval tv;
tv.tv_sec = timeout_seconds;
tv.tv_usec = 0;
if (setsockopt((int)sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
n_log(
LOG_ERR,
"Failed to set SO_RCVTIMEO: %s", strerror(errno));
return FALSE;
}
if (setsockopt((int)sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
n_log(
LOG_ERR,
"Failed to set SO_SNDTIMEO: %s", strerror(errno));
return FALSE;
}
#else
DWORD tv = (DWORD)(timeout_seconds * 1000);
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) < 0) {
return FALSE;
}
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)) < 0) {
return FALSE;
}
#endif
return TRUE;
}
int validate_path(
const char* requested_path,
const char* root,
char* resolved,
size_t resolved_size) {
char resolved_root[PATH_MAX] = "";
char resolved_request[PATH_MAX] = "";
if (!realpath(root, resolved_root)) {
n_log(
LOG_ERR,
"Cannot resolve root directory: %s (%s)", root, strerror(errno));
return FALSE;
}
if (!realpath(requested_path, resolved_request)) {
return FALSE;
}
size_t root_len = strlen(resolved_root);
if (strncmp(resolved_request, resolved_root, root_len) != 0) {
n_log(
LOG_ERR,
"Path traversal attempt blocked: %s (resolved: %s, root: %s)",
requested_path, resolved_request, resolved_root);
return FALSE;
}
if (resolved_request[root_len] != '/' && resolved_request[root_len] != '\0') {
n_log(
LOG_ERR,
"Path traversal attempt blocked (prefix match): %s", requested_path);
return FALSE;
}
strncpy(resolved, resolved_request, resolved_size - 1);
resolved[resolved_size - 1] = '\0';
return TRUE;
}
(void)recvd_signal;
#ifndef __windows__
static int nb_sigterm = 0;
switch (recvd_signal) {
case (SIGABRT):
break;
case (SIGINT):
break;
case (SIGBUS):
break;
case (SIGFPE):
break;
case (SIGSEGV):
break;
case (SIGSYS):
break;
case (SIGTERM):
nb_sigterm++;
if (nb_sigterm >= 2) {
n_log(
LOG_ERR,
"Caught too much SIGTERM, trying _exit() !!");
_exit(-1);
}
n_log(
LOG_ERR,
"Caught %d SIGTERM, exiting now !!", nb_sigterm);
exit(-1);
case (SIGUSR1):
break;
case (SIGUSR2):
break;
case (SIGHUP):
break;
default:
break;
}
#endif
}
char headers[2048] = "";
snprintf(headers,
sizeof(headers),
"%s%s",
SECURITY_HEADERS, extra_headers ? extra_headers :
"");
}
bool found = 0;
char** split_results = NULL;
char* http_url = NULL;
N_STR* dynamic_request_answer = NULL;
char* http_buffer = NULL;
if (ssl_read_ret <= 0) {
int ssl_error = SSL_get_error(netw_ptr->
ssl, ssl_read_ret);
if (ssl_error == SSL_ERROR_ZERO_RETURN) {
} else if (ssl_error == SSL_ERROR_SYSCALL && errno == EAGAIN) {
} else {
n_log(
LOG_ERR,
"SSL_read failed with SSL error %d", ssl_error);
}
return;
}
http_buffer[ssl_read_ret] = '\0';
char url[4096] = "";
split_results =
split(url,
"?", 0);
if (!split_results || !split_results[0]) {
http_body =
char_to_nstr(
"<html><body><h1>Bad Request</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a Bad Request answer for %s", url);
}
} else {
http_url = split_results[0];
if (strcmp(
"OPTIONS", http_request.
type) == 0) {
n_log(
LOG_ERR,
"couldn't build an OPTION answer for %s", url);
}
}
else if (strcmp(
"GET", http_request.
type) == 0) {
char system_url[4096] = "";
snprintf(system_url, sizeof(system_url), "%s%s", effective_root, http_url);
char validated_path[PATH_MAX] = "";
if (!
validate_path(system_url, effective_root, validated_path,
sizeof(validated_path))) {
struct stat st;
if (stat(system_url, &st) == 0) {
http_body =
char_to_nstr(
"<html><body><h1>403 Forbidden</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a Forbidden answer for %s", url);
}
} else {
http_body =
char_to_nstr(
"<html><body><h1>404 Not Found</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a NOT FOUND answer for %s", url);
}
}
} else {
struct stat st;
if (stat(validated_path, &st) == 0 && S_ISREG(st.st_mode)) {
if (!http_body) {
http_body =
char_to_nstr(
"<html><body><h1>Internal Server Error</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build an Internal Server Error answer for %s", url);
}
} else {
n_log(
LOG_ERR,
"couldn't build an http answer for %s", url);
}
}
} else if (stat(validated_path, &st) == 0 && S_ISDIR(st.st_mode)) {
char index_path[PATH_MAX + 16] = "";
size_t url_len = strlen(http_url);
const char* url_slash = (url_len > 0 && http_url[url_len - 1] == '/') ? "" : "/";
size_t vp_len = strlen(validated_path);
if (vp_len + sizeof("/index.html") > sizeof(index_path)) {
n_log(
LOG_ERR,
"path too long for index lookup: %s", validated_path);
http_body =
char_to_nstr(
"<html><body><h1>414 URI Too Long</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a URI Too Long answer for %s", url);
}
} else {
snprintf(index_path, sizeof(index_path), "%s/index.html", validated_path);
char validated_index[PATH_MAX] = "";
struct stat idx_st;
if (
validate_path(index_path, effective_root, validated_index,
sizeof(validated_index)) &&
stat(validated_index, &idx_st) == 0 && S_ISREG(idx_st.st_mode)) {
char location_header[4096] = "";
snprintf(location_header, sizeof(location_header), "Location: %s%sindex.html\r\n", http_url, url_slash);
n_log(
LOG_ERR,
"couldn't build a redirect answer for %s", url);
}
n_log(
LOG_INFO,
"%s: %s %s 301 -> %s%sindex.html",
_nstr(origin), http_request.
type, url, http_url, url_slash);
} else {
http_body =
char_to_nstr(
"<html><body><h1>404 Not Found</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a NOT FOUND answer for %s", url);
}
}
}
} else {
http_body =
char_to_nstr(
"<html><body><h1>404 Not Found</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a NOT FOUND answer for %s", url);
}
}
}
}
else if (strcmp(
"POST", http_request.
type) == 0) {
found = 0;
if (strcmp(node->ptr, http_url) == 0) {
if (post_data) {
{
});
}
n_log(
LOG_ERR,
"couldn't build a route 200 answer for %s", url);
}
found = 1;
break;
}
}
if (!found) {
http_body =
char_to_nstr(
"<html><body><h1>404 Not Found</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a NOT FOUND answer for %s", url);
}
}
} else {
http_body =
char_to_nstr(
"<html><body><h1>400 Bad Request</h1></body></html>");
n_log(
LOG_ERR,
"couldn't build a Bad Request answer for %s", url);
}
}
}
if (dynamic_request_answer) {
if (dynamic_request_answer->
written > UINT32_MAX) {
n_log(
LOG_ERR,
"response too large to send for %s: %s %s (size: %zu)",
_nstr(origin), http_request.
type, url, dynamic_request_answer->
written);
}
} else {
}
}
return NULL;
}
int main(
int argc,
char* argv[]) {
int exit_code = 0;
process_args(argc, argv, &
addr, &
port, &
key, &
cert, &
ca_file, &
ssl_verify,
routes, &
ip_version, &
max_http_request_size, &
root_dir);
exit_code = 1;
goto clean_and_exit;
}
exit_code = 1;
goto clean_and_exit;
}
exit_code = 1;
goto clean_and_exit;
}
#ifndef __windows__
errno = 0;
signal(SIGPIPE, SIG_IGN);
struct sigaction signal_catcher;
sigemptyset(&signal_catcher.sa_mask);
signal_catcher.sa_flags = 0;
sigaction(SIGTERM, &signal_catcher, NULL);
sigaction(SIGUSR1, &signal_catcher, NULL);
#endif
int nb_active_threads = (cores > 0) ? (int)cores : 1;
int nb_waiting_threads = 10 * nb_active_threads;
n_log(
LOG_INFO,
"Creating a new thread pool of %d active and %d waiting threads", nb_active_threads, nb_waiting_threads);
exit(-1);
}
} else {
}
exit_code = 1;
goto clean_and_exit;
}
}
int accepted_count = 0;
int return_code = 0;
if (return_code == EINTR) {
goto clean_and_exit;
} else {
}
} else {
n_log(
LOG_ERR,
"Error adding client management to thread pool");
}
accepted_count++;
break;
}
}
}
clean_and_exit:
}
netw_unload();
exit(exit_code);
}
void process_args(int argc, char **argv)
THREAD_POOL * thread_pool
NETWORK * netw
Network for server mode, accepting incomming.
int max_http_request_size
void * ssl_network_thread(void *params)
void action_on_sig(int recvd_signal)
void handle_request(NETWORK *netw_ptr, LIST *routes_ptr)
int ssl_harden_context(NETWORK *ssl_netw)
Harden the SSL context with modern security settings.
#define CONNECTION_TIMEOUT_SECONDS
#define SECURITY_HEADERS
Network for managing connections.
LIST * routes
virtual routes for the server
#define MAX_HTTP_REQUEST_SIZE_LIMIT
int set_socket_timeout(SOCKET sock, int timeout_seconds)
Set socket read/write timeout to prevent slow-loris attacks.
NETWORK * netw
network to use for the receiving thread
int validate_path(const char *requested_path, const char *root, char *resolved, size_t resolved_size)
Validate that a resolved path stays within the document root.
int build_secure_response(N_STR **response, int status_code, const char *content_type, char *extra_headers, N_STR *body)
structure of a NETWORK_SSL_THREAD_PARAMS
#define FreeNoLog(__ptr)
Free Handler without log.
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
#define __n_assert(__ptr, __ret)
macro to assert things
#define _str(__PTR)
define true
#define Free(__ptr)
Free Handler to get errors.
#define _nstr(__PTR)
N_STR or "NULL" string for logging purposes.
int destroy_ht(HASH_TABLE **table)
empty a table and destroy it
#define HT_FOREACH(__ITEM_, __HASH_,...)
ForEach macro helper.
structure of a hash table
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
#define list_foreach(__ITEM_, __LIST_)
ForEach macro helper, safe for node removal during iteration.
int list_destroy(LIST **list)
Empty and Free a list container.
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
#define MAX_LIST_ITEMS
flag to pass to new_generic_list for the maximum possible number of item in a list
Structure of a generic LIST container.
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
#define LOG_DEBUG
debug-level messages
#define LOG_ERR
error conditions
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL )
#define LOG_NOTICE
normal but significant condition
#define LOG_NULL
no log output
#define LOG_INFO
informational
size_t written
size of the written data inside the string
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
N_STR * char_to_nstr(const char *src)
Convert a char into a N_STR, short version.
N_STR * new_nstr(NSTRBYTE size)
create a new N_STR string
#define nstrprintf(__nstr_var, __format,...)
Macro to quickly allocate and sprintf to N_STR.
char ** split(const char *str, const char *delim, int empty)
split the strings into a an array of char *pointer , ended by a NULL one.
N_STR * file_to_nstr(char *filename)
Load a whole file into a N_STR.
int free_split_result(char ***tab)
Free a split result allocated array.
A box including a string and his lenght.
char * ip
ip of the connected socket
N_SOCKET link
networking socket
char * type
Type of request.
char * body
Pointer to the body data.
SOCKET sock
a normal socket
SSL_CTX * ctx
SSL context holder.
ssize_t send_ssl_data(void *netw, char *buf, uint32_t n)
send data onto the socket
int netw_ssl_set_verify(NETWORK *netw, int enable)
enable or disable SSL peer certificate verification
int netw_get_url_from_http_request(const char *request, char *url, size_t size)
Helper function to extract the URL from the HTTP request line.
int netw_set_crypto(NETWORK *netw, char *key, char *certificate)
activate SSL encryption on selected network, using key and certificate
#define NETWORK_IPV6
Flag to force IPV6
int netw_make_listening(NETWORK **netw, char *addr, char *port, int nbpending, int ip_version)
Make a NETWORK be a Listening network.
#define NETWORK_IPV4
Flag to force IPV4
int netw_build_http_response(N_STR **http_response, int status_code, const char *server_name, const char *content_type, char *additional_headers, N_STR *body)
function to dynamically generate an HTTP response
#define SOCKET_SIZE_FORMAT
socket associated printf style
NETWORK * netw_accept_from_ex(NETWORK *from, size_t send_list_limit, size_t recv_list_limit, int blocking, int *retval)
make a normal 'accept' .
#define NETWORK_IPALL
Flag for auto detection by OS of ip version to use.
int SOCKET
default socket declaration
int netw_close(NETWORK **netw)
Closing a specified Network, destroy queues, free the structure.
int netw_set_crypto_chain(NETWORK *netw, char *key, char *certificate, char *ca_file)
activate SSL encryption using key/certificate files and a CA file for chain verification
NETWORK_HTTP_INFO netw_extract_http_info(char *request)
extract a lot of informations, mostly as pointers, and populate a NETWORK_HTTP_INFO structure
int netw_info_destroy(NETWORK_HTTP_INFO http_request)
destroy a NETWORK_HTTP_INFO loaded informations
HASH_TABLE * netw_parse_post_data(const char *post_data)
Function to parse POST data.
const char * netw_guess_http_content_type(const char *url)
function to guess the content type based on URL extension
structure for splitting HTTP requests
THREAD_POOL * new_thread_pool(size_t nbmaxthr, size_t nb_max_waiting)
Create a new pool of nbmaxthr threads.
int add_threaded_process(THREAD_POOL *thread_pool, void *(*func_ptr)(void *param), void *param, int mode)
add a function and params to a thread pool
int wait_for_threaded_pool(THREAD_POOL *thread_pool)
Wait for the thread pool to become idle (no active threads, empty waiting list), blocking without pol...
int destroy_threaded_pool(THREAD_POOL **pool, unsigned int delay)
delete a thread_pool, exit the threads and free the structs
long int get_nb_cpu_cores()
get number of core of current system
#define DIRECT_PROC
processing mode for added func, direct start, not queued
Structure of a thread pool.
List structures and definitions.
Signals general handling with stack printing, from https://gist.github.com/jvranish/4441299.
N_STR and string function declaration.