From 58daab21cd043e1dc37024a7f99b396788372918 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 9 Mar 2024 14:19:48 +0100 Subject: Merging upstream version 1.44.3. Signed-off-by: Daniel Baumann --- web/server/h2o/libh2o/src/main.c | 2239 ++++++++++++++++++++++++++++++++ web/server/h2o/libh2o/src/setuidgid.c | 78 ++ web/server/h2o/libh2o/src/ssl.c | 962 ++++++++++++++ web/server/h2o/libh2o/src/standalone.h | 37 + 4 files changed, 3316 insertions(+) create mode 100644 web/server/h2o/libh2o/src/main.c create mode 100644 web/server/h2o/libh2o/src/setuidgid.c create mode 100644 web/server/h2o/libh2o/src/ssl.c create mode 100644 web/server/h2o/libh2o/src/standalone.h (limited to 'web/server/h2o/libh2o/src') diff --git a/web/server/h2o/libh2o/src/main.c b/web/server/h2o/libh2o/src/main.c new file mode 100644 index 000000000..af0867f29 --- /dev/null +++ b/web/server/h2o/libh2o/src/main.c @@ -0,0 +1,2239 @@ +/* + * Copyright (c) 2014-2016 DeNA Co., Ltd., Kazuho Oku, Tatsuhiko Kubo, + * Domingo Alvarez Duarte, Nick Desaulniers, + * Jeff Marrison, Shota Fukumori, Fastly, Inc. + * + * 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __GLIBC__ +#include +#endif +#if H2O_USE_PICOTLS +#include "picotls.h" +#include "picotls/minicrypto.h" +#include "picotls/openssl.h" +#endif +#include "cloexec.h" +#include "yoml-parser.h" +#include "neverbleed.h" +#include "h2o.h" +#include "h2o/configurator.h" +#include "h2o/http1.h" +#include "h2o/http2.h" +#include "h2o/serverutil.h" +#if H2O_USE_MRUBY +#include "h2o/mruby_.h" +#endif +#include "standalone.h" + +#ifdef TCP_FASTOPEN +#define H2O_DEFAULT_LENGTH_TCP_FASTOPEN_QUEUE 4096 +#else +#define H2O_DEFAULT_LENGTH_TCP_FASTOPEN_QUEUE 0 +#endif + +#define H2O_DEFAULT_NUM_NAME_RESOLUTION_THREADS 32 + +#define H2O_DEFAULT_OCSP_UPDATER_MAX_THREADS 10 + +#if defined(OPENSSL_NO_OCSP) && !H2O_USE_PICOTLS +#define H2O_USE_OCSP 0 +#else +#define H2O_USE_OCSP 1 +#endif + +struct listener_ssl_config_t { + H2O_VECTOR(h2o_iovec_t) hostnames; + char *certificate_file; + SSL_CTX *ctx; +#if H2O_USE_OCSP + struct { + uint64_t interval; + unsigned max_failures; + char *cmd; + pthread_t updater_tid; /* should be valid when and only when interval != 0 */ + struct { + pthread_mutex_t mutex; + h2o_buffer_t *data; + } response; + } ocsp_stapling; +#endif +}; + +struct listener_config_t { + int fd; + struct sockaddr_storage addr; + socklen_t addrlen; + h2o_hostconf_t **hosts; + H2O_VECTOR(struct listener_ssl_config_t *) ssl; + int proxy_protocol; +}; + +struct listener_ctx_t { + h2o_accept_ctx_t accept_ctx; + h2o_socket_t *sock; +}; + +typedef struct st_resolve_tag_node_cache_entry_t { + h2o_iovec_t filename; + yoml_t *node; +} resolve_tag_node_cache_entry_t; + +typedef struct st_resolve_tag_arg_t { + H2O_VECTOR(resolve_tag_node_cache_entry_t) node_cache; +} resolve_tag_arg_t; + +typedef enum en_run_mode_t { + RUN_MODE_WORKER = 0, + RUN_MODE_MASTER, + RUN_MODE_DAEMON, + RUN_MODE_TEST, +} run_mode_t; + +static struct { + h2o_globalconf_t globalconf; + run_mode_t run_mode; + struct { + int *fds; + char *bound_fd_map; /* has `num_fds` elements, set to 1 if fd[index] was bound to one of the listeners */ + size_t num_fds; + char *env_var; + } server_starter; + struct listener_config_t **listeners; + size_t num_listeners; + char *pid_file; + char *error_log; + int max_connections; + size_t num_threads; + int tfo_queues; + time_t launch_time; + struct { + pthread_t tid; + h2o_context_t ctx; + h2o_multithread_receiver_t server_notifications; + h2o_multithread_receiver_t memcached; + } * threads; + volatile sig_atomic_t shutdown_requested; + h2o_barrier_t startup_sync_barrier; + struct { + /* unused buffers exist to avoid false sharing of the cache line */ + char _unused1_avoir_false_sharing[32]; + int _num_connections; /* number of currently handled incoming connections, should use atomic functions to update the value + */ + char _unused2_avoir_false_sharing[32]; + unsigned long + _num_sessions; /* total number of opened incoming connections, should use atomic functions to update the value */ + char _unused3_avoir_false_sharing[32]; + } state; + char *crash_handler; + int crash_handler_wait_pipe_close; +} conf = { + {NULL}, /* globalconf */ + RUN_MODE_WORKER, /* dry-run */ + {NULL}, /* server_starter */ + NULL, /* listeners */ + 0, /* num_listeners */ + NULL, /* pid_file */ + NULL, /* error_log */ + 1024, /* max_connections */ + 0, /* initialized in main() */ + 0, /* initialized in main() */ + 0, /* initialized in main() */ + NULL, /* thread_ids */ + 0, /* shutdown_requested */ + H2O_BARRIER_INITIALIZER(SIZE_MAX), /* startup_sync_barrier */ + {{0}}, /* state */ + "share/h2o/annotate-backtrace-symbols", /* crash_handler */ + 0, /* crash_handler_wait_pipe_close */ +}; + +static neverbleed_t *neverbleed = NULL; + +static void set_cloexec(int fd) +{ + if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) { + perror("failed to set FD_CLOEXEC"); + abort(); + } +} + +static int on_openssl_print_errors(const char *str, size_t len, void *fp) +{ + fwrite(str, 1, len, fp); + return (int)len; +} + +static void setup_ecc_key(SSL_CTX *ssl_ctx) +{ +#ifdef SSL_CTX_set_ecdh_auto + SSL_CTX_set_ecdh_auto(ssl_ctx, 1); +#else + int nid = NID_X9_62_prime256v1; + EC_KEY *key = EC_KEY_new_by_curve_name(nid); + if (key == NULL) { + fprintf(stderr, "Failed to create curve \"%s\"\n", OBJ_nid2sn(nid)); + return; + } + SSL_CTX_set_tmp_ecdh(ssl_ctx, key); + EC_KEY_free(key); +#endif +} + +static struct listener_ssl_config_t *resolve_sni(struct listener_config_t *listener, const char *name, size_t name_len) +{ + size_t i, j; + + for (i = 0; i != listener->ssl.size; ++i) { + struct listener_ssl_config_t *ssl_config = listener->ssl.entries[i]; + for (j = 0; j != ssl_config->hostnames.size; ++j) { + if (ssl_config->hostnames.entries[j].base[0] == '*') { + /* matching against "*.foo.bar" */ + size_t cmplen = ssl_config->hostnames.entries[j].len - 1; + if (!(cmplen < name_len && h2o_lcstris(name + name_len - cmplen, cmplen, ssl_config->hostnames.entries[j].base + 1, + ssl_config->hostnames.entries[j].len - 1))) + continue; + } else { + if (!h2o_lcstris(name, name_len, ssl_config->hostnames.entries[j].base, ssl_config->hostnames.entries[j].len)) + continue; + } + /* found */ + return listener->ssl.entries[i]; + } + } + return listener->ssl.entries[0]; +} + +static int on_sni_callback(SSL *ssl, int *ad, void *arg) +{ + struct listener_config_t *listener = arg; + const char *server_name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + + if (server_name != NULL) { + struct listener_ssl_config_t *resolved = resolve_sni(listener, server_name, strlen(server_name)); + if (resolved->ctx != SSL_get_SSL_CTX(ssl)) + SSL_set_SSL_CTX(ssl, resolved->ctx); + } + + return SSL_TLSEXT_ERR_OK; +} + +#if H2O_USE_PICOTLS +struct st_on_client_hello_ptls_t { + ptls_on_client_hello_t super; + struct listener_config_t *listener; +}; + +static int on_client_hello_ptls(ptls_on_client_hello_t *_self, ptls_t *tls, ptls_iovec_t server_name, + const ptls_iovec_t *negotiated_protocols, size_t num_negotiated_protocols, + const uint16_t *signature_algorithms, size_t num_signature_algorithms) +{ + struct st_on_client_hello_ptls_t *self = (struct st_on_client_hello_ptls_t *)_self; + int ret = 0; + + /* handle SNI */ + if (server_name.base != NULL) { + struct listener_ssl_config_t *resolved = resolve_sni(self->listener, (const char *)server_name.base, server_name.len); + ptls_context_t *newctx = h2o_socket_ssl_get_picotls_context(resolved->ctx); + ptls_set_context(tls, newctx); + ptls_set_server_name(tls, (const char *)server_name.base, server_name.len); + } + + /* handle ALPN */ + if (num_negotiated_protocols != 0) { + const h2o_iovec_t *server_pref; + for (server_pref = h2o_alpn_protocols; server_pref->len != 0; ++server_pref) { + size_t i; + for (i = 0; i != num_negotiated_protocols; ++i) + if (h2o_memis(server_pref->base, server_pref->len, negotiated_protocols[i].base, negotiated_protocols[i].len)) + goto ALPN_Found; + } + return PTLS_ALERT_NO_APPLICATION_PROTOCOL; + ALPN_Found: + if ((ret = ptls_set_negotiated_protocol(tls, server_pref->base, server_pref->len)) != 0) + return ret; + } + + return ret; +} +#endif + +static void update_ocsp_stapling(struct listener_ssl_config_t *ssl_conf, h2o_buffer_t *resp) +{ + pthread_mutex_lock(&ssl_conf->ocsp_stapling.response.mutex); + if (ssl_conf->ocsp_stapling.response.data != NULL) + h2o_buffer_dispose(&ssl_conf->ocsp_stapling.response.data); + ssl_conf->ocsp_stapling.response.data = resp; + pthread_mutex_unlock(&ssl_conf->ocsp_stapling.response.mutex); +} + +static int get_ocsp_response(const char *cert_fn, const char *cmd, h2o_buffer_t **resp) +{ + char *cmd_fullpath = h2o_configurator_get_cmd_path(cmd), *argv[] = {cmd_fullpath, (char *)cert_fn, NULL}; + int child_status, ret; + + if (h2o_read_command(cmd_fullpath, argv, resp, &child_status) != 0) { + fprintf(stderr, "[OCSP Stapling] failed to execute %s:%s\n", cmd, strerror(errno)); + switch (errno) { + case EACCES: + case ENOENT: + case ENOEXEC: + /* permanent errors */ + ret = EX_CONFIG; + goto Exit; + default: + ret = EX_TEMPFAIL; + goto Exit; + } + } + + if (!(WIFEXITED(child_status) && WEXITSTATUS(child_status) == 0)) + h2o_buffer_dispose(resp); + if (!WIFEXITED(child_status)) { + fprintf(stderr, "[OCSP Stapling] command %s was killed by signal %d\n", cmd_fullpath, WTERMSIG(child_status)); + ret = EX_TEMPFAIL; + goto Exit; + } + ret = WEXITSTATUS(child_status); + +Exit: + free(cmd_fullpath); + return ret; +} + +static h2o_sem_t ocsp_updater_semaphore; + +static void *ocsp_updater_thread(void *_ssl_conf) +{ + struct listener_ssl_config_t *ssl_conf = _ssl_conf; + time_t next_at = 0, now; + unsigned fail_cnt = 0; + int status; + h2o_buffer_t *resp; + + assert(ssl_conf->ocsp_stapling.interval != 0); + + while (1) { + /* sleep until next_at */ + if ((now = time(NULL)) < next_at) { + time_t sleep_secs = next_at - now; + sleep(sleep_secs < UINT_MAX ? (unsigned)sleep_secs : UINT_MAX); + continue; + } + /* fetch the response */ + h2o_sem_wait(&ocsp_updater_semaphore); + status = get_ocsp_response(ssl_conf->certificate_file, ssl_conf->ocsp_stapling.cmd, &resp); + h2o_sem_post(&ocsp_updater_semaphore); + switch (status) { + case 0: /* success */ + fail_cnt = 0; + update_ocsp_stapling(ssl_conf, resp); + fprintf(stderr, "[OCSP Stapling] successfully updated the response for certificate file:%s\n", + ssl_conf->certificate_file); + break; + case EX_TEMPFAIL: /* temporary failure */ + if (fail_cnt == ssl_conf->ocsp_stapling.max_failures) { + fprintf(stderr, + "[OCSP Stapling] OCSP stapling is temporary disabled due to repeated errors for certificate file:%s\n", + ssl_conf->certificate_file); + update_ocsp_stapling(ssl_conf, NULL); + } else { + fprintf(stderr, "[OCSP Stapling] reusing old response due to a temporary error occurred while fetching OCSP " + "response for certificate file:%s\n", + ssl_conf->certificate_file); + ++fail_cnt; + } + break; + default: /* permanent failure */ + update_ocsp_stapling(ssl_conf, NULL); + fprintf(stderr, "[OCSP Stapling] disabled for certificate file:%s\n", ssl_conf->certificate_file); + goto Exit; + } + /* update next_at */ + next_at = time(NULL) + ssl_conf->ocsp_stapling.interval; + } + +Exit: + return NULL; +} + +#ifndef OPENSSL_NO_OCSP + +static int on_staple_ocsp_ossl(SSL *ssl, void *_ssl_conf) +{ + struct listener_ssl_config_t *ssl_conf = _ssl_conf; + void *resp = NULL; + size_t len = 0; + + /* fetch ocsp response */ + pthread_mutex_lock(&ssl_conf->ocsp_stapling.response.mutex); + if (ssl_conf->ocsp_stapling.response.data != NULL) { + resp = CRYPTO_malloc((int)ssl_conf->ocsp_stapling.response.data->size, __FILE__, __LINE__); + if (resp != NULL) { + len = ssl_conf->ocsp_stapling.response.data->size; + memcpy(resp, ssl_conf->ocsp_stapling.response.data->bytes, len); + } + } + pthread_mutex_unlock(&ssl_conf->ocsp_stapling.response.mutex); + + if (resp != NULL) { + SSL_set_tlsext_status_ocsp_resp(ssl, resp, len); + return SSL_TLSEXT_ERR_OK; + } else { + return SSL_TLSEXT_ERR_NOACK; + } +} + +#endif + +#if H2O_USE_PICOTLS + +struct st_staple_ocsp_ptls_t { + ptls_staple_ocsp_t super; + struct listener_ssl_config_t *conf; +}; + +static int on_staple_ocsp_ptls(ptls_staple_ocsp_t *_self, ptls_t *tls, ptls_buffer_t *output, size_t cert_index) +{ + struct st_staple_ocsp_ptls_t *self = (struct st_staple_ocsp_ptls_t *)_self; + int locked = 0, ret; + + if (cert_index != 0) { + ret = PTLS_ERROR_LIBRARY; + goto Exit; + } + + pthread_mutex_lock(&self->conf->ocsp_stapling.response.mutex); + locked = 1; + + if (self->conf->ocsp_stapling.response.data == NULL) { + ret = PTLS_ERROR_LIBRARY; + goto Exit; + } + ptls_buffer_pushv(output, self->conf->ocsp_stapling.response.data->bytes, self->conf->ocsp_stapling.response.data->size); + ret = 0; + +Exit: + if (locked) + pthread_mutex_unlock(&self->conf->ocsp_stapling.response.mutex); + return ret; +} + +static const char *listener_setup_ssl_picotls(struct listener_config_t *listener, struct listener_ssl_config_t *ssl_config, + SSL_CTX *ssl_ctx) +{ + static const ptls_key_exchange_algorithm_t *key_exchanges[] = {&ptls_minicrypto_x25519, &ptls_openssl_secp256r1, NULL}; + struct st_fat_context_t { + ptls_context_t ctx; + struct st_on_client_hello_ptls_t ch; + struct st_staple_ocsp_ptls_t so; + ptls_openssl_sign_certificate_t sc; + } *pctx = h2o_mem_alloc(sizeof(*pctx)); + EVP_PKEY *key; + X509 *cert; + STACK_OF(X509) * cert_chain; + int ret; + + *pctx = (struct st_fat_context_t){{ptls_openssl_random_bytes, + &ptls_get_time, + key_exchanges, + ptls_openssl_cipher_suites, + {NULL, 0}, + &pctx->ch.super, + &pctx->so.super, + &pctx->sc.super, + NULL, + 0, + 8192, + 1}, + {{on_client_hello_ptls}, listener}, + {{on_staple_ocsp_ptls}, ssl_config}}; + + { /* obtain key and cert (via fake connection for libressl compatibility) */ + SSL *fakeconn = SSL_new(ssl_ctx); + assert(fakeconn != NULL); + key = SSL_get_privatekey(fakeconn); + assert(key != NULL); + cert = SSL_get_certificate(fakeconn); + assert(cert != NULL); + SSL_free(fakeconn); + } + + if (ptls_openssl_init_sign_certificate(&pctx->sc, key) != 0) { + free(pctx); + return "failed to setup private key"; + } + + SSL_CTX_get_extra_chain_certs(ssl_ctx, &cert_chain); + ret = ptls_openssl_load_certificates(&pctx->ctx, cert, cert_chain); + assert(ret == 0); + + h2o_socket_ssl_set_picotls_context(ssl_ctx, &pctx->ctx); + + return NULL; +} + +#endif + +static void listener_setup_ssl_add_host(struct listener_ssl_config_t *ssl_config, h2o_iovec_t host) +{ + const char *host_end = memchr(host.base, ':', host.len); + if (host_end == NULL) + host_end = host.base + host.len; + + h2o_vector_reserve(NULL, &ssl_config->hostnames, ssl_config->hostnames.size + 1); + ssl_config->hostnames.entries[ssl_config->hostnames.size++] = h2o_iovec_init(host.base, host_end - host.base); +} + +static int listener_setup_ssl(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *listen_node, + yoml_t *ssl_node, struct listener_config_t *listener, int listener_is_new) +{ + SSL_CTX *ssl_ctx = NULL; + yoml_t *certificate_file = NULL, *key_file = NULL, *dh_file = NULL, *min_version = NULL, *max_version = NULL, + *cipher_suite = NULL, *ocsp_update_cmd = NULL, *ocsp_update_interval_node = NULL, *ocsp_max_failures_node = NULL; + long ssl_options = SSL_OP_ALL; + uint64_t ocsp_update_interval = 4 * 60 * 60; /* defaults to 4 hours */ + unsigned ocsp_max_failures = 3; /* defaults to 3; permit 3 failures before temporary disabling OCSP stapling */ + int use_neverbleed = 1, use_picotls = 1; /* enabled by default */ + + if (!listener_is_new) { + if (listener->ssl.size != 0 && ssl_node == NULL) { + h2o_configurator_errprintf(cmd, listen_node, "cannot accept HTTP; already defined to accept HTTPS"); + return -1; + } + if (listener->ssl.size == 0 && ssl_node != NULL) { + h2o_configurator_errprintf(cmd, ssl_node, "cannot accept HTTPS; already defined to accept HTTP"); + return -1; + } + } + + if (ssl_node == NULL) + return 0; + if (ssl_node->type != YOML_TYPE_MAPPING) { + h2o_configurator_errprintf(cmd, ssl_node, "`ssl` is not a mapping"); + return -1; + } + + { /* parse */ + size_t i; + for (i = 0; i != ssl_node->data.sequence.size; ++i) { + yoml_t *key = ssl_node->data.mapping.elements[i].key, *value = ssl_node->data.mapping.elements[i].value; + /* obtain the target command */ + if (key->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(NULL, key, "command must be a string"); + return -1; + } +#define FETCH_PROPERTY(n, p) \ + if (strcmp(key->data.scalar, n) == 0) { \ + if (value->type != YOML_TYPE_SCALAR) { \ + h2o_configurator_errprintf(cmd, value, "property of `" n "` must be a string"); \ + return -1; \ + } \ + p = value; \ + continue; \ + } + FETCH_PROPERTY("certificate-file", certificate_file); + FETCH_PROPERTY("key-file", key_file); + FETCH_PROPERTY("min-version", min_version); + FETCH_PROPERTY("minimum-version", min_version); + FETCH_PROPERTY("max-version", max_version); + FETCH_PROPERTY("maximum-version", max_version); + FETCH_PROPERTY("cipher-suite", cipher_suite); + FETCH_PROPERTY("ocsp-update-cmd", ocsp_update_cmd); + FETCH_PROPERTY("ocsp-update-interval", ocsp_update_interval_node); + FETCH_PROPERTY("ocsp-max-failures", ocsp_max_failures_node); + FETCH_PROPERTY("dh-file", dh_file); + if (strcmp(key->data.scalar, "cipher-preference") == 0) { + if (value->type == YOML_TYPE_SCALAR && strcasecmp(value->data.scalar, "client") == 0) { + ssl_options &= ~SSL_OP_CIPHER_SERVER_PREFERENCE; + } else if (value->type == YOML_TYPE_SCALAR && strcasecmp(value->data.scalar, "server") == 0) { + ssl_options |= SSL_OP_CIPHER_SERVER_PREFERENCE; + } else { + h2o_configurator_errprintf(cmd, value, "property of `cipher-preference` must be either of: `client`, `server`"); + return -1; + } + continue; + } + if (strcmp(key->data.scalar, "neverbleed") == 0) { + if (value->type == YOML_TYPE_SCALAR && strcasecmp(value->data.scalar, "ON") == 0) { + /* no need to enable neverbleed for daemon / master */ + use_neverbleed = 1; + } else if (value->type == YOML_TYPE_SCALAR && strcasecmp(value->data.scalar, "OFF") == 0) { + use_neverbleed = 0; + } else { + h2o_configurator_errprintf(cmd, value, "property of `neverbleed` must be either of: `ON`, `OFF"); + return -1; + } + continue; + } + h2o_configurator_errprintf(cmd, key, "unknown property: %s", key->data.scalar); + return -1; +#undef FETCH_PROPERTY + } + if (certificate_file == NULL) { + h2o_configurator_errprintf(cmd, ssl_node, "could not find mandatory property `certificate-file`"); + return -1; + } + if (key_file == NULL) { + h2o_configurator_errprintf(cmd, ssl_node, "could not find mandatory property `key-file`"); + return -1; + } + if (min_version != NULL) { +#define MAP(tok, op) \ + if (strcasecmp(min_version->data.scalar, tok) == 0) { \ + ssl_options |= (op); \ + goto VersionFound; \ + } + MAP("sslv2", 0); + MAP("sslv3", SSL_OP_NO_SSLv2); + MAP("tlsv1", SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + MAP("tlsv1.1", SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1); +#ifdef SSL_OP_NO_TLSv1_1 + MAP("tlsv1.2", SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1); +#endif +#ifdef SSL_OP_NO_TLSv1_2 + MAP("tlsv1.3", SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2); +#endif +#undef MAP + h2o_configurator_errprintf(cmd, min_version, "unknown protocol version: %s", min_version->data.scalar); + VersionFound:; + } else { + /* default is >= TLSv1 */ + ssl_options |= SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; + } + if (max_version != NULL) { + if (strcasecmp(max_version->data.scalar, "tlsv1.3") < 0) + use_picotls = 0; + } + if (ocsp_update_interval_node != NULL) { + if (h2o_configurator_scanf(cmd, ocsp_update_interval_node, "%" PRIu64, &ocsp_update_interval) != 0) + goto Error; + } + if (ocsp_max_failures_node != NULL) { + if (h2o_configurator_scanf(cmd, ocsp_max_failures_node, "%u", &ocsp_max_failures) != 0) + goto Error; + } + } + + /* add the host to the existing SSL config, if the certificate file is already registered */ + if (ctx->hostconf != NULL) { + size_t i; + for (i = 0; i != listener->ssl.size; ++i) { + struct listener_ssl_config_t *ssl_config = listener->ssl.entries[i]; + if (strcmp(ssl_config->certificate_file, certificate_file->data.scalar) == 0) { + listener_setup_ssl_add_host(ssl_config, ctx->hostconf->authority.hostport); + return 0; + } + } + } + +/* disable tls compression to avoid "CRIME" attacks (see http://en.wikipedia.org/wiki/CRIME) */ +#ifdef SSL_OP_NO_COMPRESSION + ssl_options |= SSL_OP_NO_COMPRESSION; +#endif + + /* setup */ + ssl_ctx = SSL_CTX_new(SSLv23_server_method()); + SSL_CTX_set_options(ssl_ctx, ssl_options); + + setup_ecc_key(ssl_ctx); + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, certificate_file->data.scalar) != 1) { + h2o_configurator_errprintf(cmd, certificate_file, "failed to load certificate file:%s\n", certificate_file->data.scalar); + ERR_print_errors_cb(on_openssl_print_errors, stderr); + goto Error; + } + if (use_neverbleed) { + /* disable neverbleed in case the process is not going to serve requests */ + switch (conf.run_mode) { + case RUN_MODE_DAEMON: + case RUN_MODE_MASTER: + use_neverbleed = 0; + break; + default: + break; + } + } + if (use_neverbleed) { + char errbuf[NEVERBLEED_ERRBUF_SIZE]; + if (neverbleed == NULL) { + neverbleed = h2o_mem_alloc(sizeof(*neverbleed)); + if (neverbleed_init(neverbleed, errbuf) != 0) { + fprintf(stderr, "%s\n", errbuf); + abort(); + } + } + if (neverbleed_load_private_key_file(neverbleed, ssl_ctx, key_file->data.scalar, errbuf) != 1) { + h2o_configurator_errprintf(cmd, key_file, "failed to load private key file:%s:%s\n", key_file->data.scalar, errbuf); + goto Error; + } + } else { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file->data.scalar, SSL_FILETYPE_PEM) != 1) { + h2o_configurator_errprintf(cmd, key_file, "failed to load private key file:%s\n", key_file->data.scalar); + ERR_print_errors_cb(on_openssl_print_errors, stderr); + goto Error; + } + } + if (cipher_suite != NULL && SSL_CTX_set_cipher_list(ssl_ctx, cipher_suite->data.scalar) != 1) { + h2o_configurator_errprintf(cmd, cipher_suite, "failed to setup SSL cipher suite\n"); + ERR_print_errors_cb(on_openssl_print_errors, stderr); + goto Error; + } + if (dh_file != NULL) { + BIO *bio = BIO_new_file(dh_file->data.scalar, "r"); + if (bio == NULL) { + h2o_configurator_errprintf(cmd, dh_file, "failed to load dhparam file:%s\n", dh_file->data.scalar); + ERR_print_errors_cb(on_openssl_print_errors, stderr); + goto Error; + } + DH *dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL); + BIO_free(bio); + if (dh == NULL) { + h2o_configurator_errprintf(cmd, dh_file, "failed to load dhparam file:%s\n", dh_file->data.scalar); + ERR_print_errors_cb(on_openssl_print_errors, stderr); + goto Error; + } + SSL_CTX_set_tmp_dh(ssl_ctx, dh); + SSL_CTX_set_options(ssl_ctx, SSL_OP_SINGLE_DH_USE); + DH_free(dh); + } + +/* setup protocol negotiation methods */ +#if H2O_USE_NPN + h2o_ssl_register_npn_protocols(ssl_ctx, h2o_npn_protocols); +#endif +#if H2O_USE_ALPN + h2o_ssl_register_alpn_protocols(ssl_ctx, h2o_alpn_protocols); +#endif + + /* set SNI callback to the first SSL context, when and only when it should be used */ + if (listener->ssl.size == 1) { + SSL_CTX_set_tlsext_servername_callback(listener->ssl.entries[0]->ctx, on_sni_callback); + SSL_CTX_set_tlsext_servername_arg(listener->ssl.entries[0]->ctx, listener); + } + + /* create a new entry in the SSL context list */ + struct listener_ssl_config_t *ssl_config = h2o_mem_alloc(sizeof(*ssl_config)); + memset(ssl_config, 0, sizeof(*ssl_config)); + h2o_vector_reserve(NULL, &listener->ssl, listener->ssl.size + 1); + listener->ssl.entries[listener->ssl.size++] = ssl_config; + if (ctx->hostconf != NULL) { + listener_setup_ssl_add_host(ssl_config, ctx->hostconf->authority.hostport); + } + ssl_config->ctx = ssl_ctx; + ssl_config->certificate_file = h2o_strdup(NULL, certificate_file->data.scalar, SIZE_MAX).base; + +#if !H2O_USE_OCSP + if (ocsp_update_interval != 0) + fprintf(stderr, "[OCSP Stapling] disabled (not support by the SSL library)\n"); +#else +#ifndef OPENSSL_NO_OCSP + SSL_CTX_set_tlsext_status_cb(ssl_ctx, on_staple_ocsp_ossl); + SSL_CTX_set_tlsext_status_arg(ssl_ctx, ssl_config); +#endif + pthread_mutex_init(&ssl_config->ocsp_stapling.response.mutex, NULL); + ssl_config->ocsp_stapling.cmd = + ocsp_update_cmd != NULL ? h2o_strdup(NULL, ocsp_update_cmd->data.scalar, SIZE_MAX).base : "share/h2o/fetch-ocsp-response"; + if (ocsp_update_interval != 0) { + switch (conf.run_mode) { + case RUN_MODE_WORKER: + ssl_config->ocsp_stapling.interval = + ocsp_update_interval; /* is also used as a flag for indicating if the updater thread was spawned */ + ssl_config->ocsp_stapling.max_failures = ocsp_max_failures; + h2o_multithread_create_thread(&ssl_config->ocsp_stapling.updater_tid, NULL, ocsp_updater_thread, ssl_config); + break; + case RUN_MODE_MASTER: + case RUN_MODE_DAEMON: + /* nothing to do */ + break; + case RUN_MODE_TEST: { + h2o_buffer_t *respbuf; + fprintf(stderr, "[OCSP Stapling] testing for certificate file:%s\n", certificate_file->data.scalar); + switch (get_ocsp_response(certificate_file->data.scalar, ssl_config->ocsp_stapling.cmd, &respbuf)) { + case 0: + h2o_buffer_dispose(&respbuf); + fprintf(stderr, "[OCSP Stapling] stapling works for file:%s\n", certificate_file->data.scalar); + break; + case EX_TEMPFAIL: + h2o_configurator_errprintf(cmd, certificate_file, "[OCSP Stapling] temporary failed for file:%s\n", + certificate_file->data.scalar); + break; + default: + h2o_configurator_errprintf(cmd, certificate_file, "[OCSP Stapling] does not work, will be disabled for file:%s\n", + certificate_file->data.scalar); + break; + } + } break; + } + } +#endif + +#if H2O_USE_PICOTLS + if (use_picotls) { + const char *errstr = listener_setup_ssl_picotls(listener, ssl_config, ssl_ctx); + if (errstr != NULL) + h2o_configurator_errprintf(cmd, ssl_node, "%s; TLS 1.3 will be disabled\n", errstr); + } +#endif + + return 0; + +Error: + if (ssl_ctx != NULL) + SSL_CTX_free(ssl_ctx); + return -1; +} + +static struct listener_config_t *find_listener(struct sockaddr *addr, socklen_t addrlen) +{ + size_t i; + + for (i = 0; i != conf.num_listeners; ++i) { + struct listener_config_t *listener = conf.listeners[i]; + if (listener->addrlen == addrlen && h2o_socket_compare_address((void *)&listener->addr, addr) == 0) + return listener; + } + + return NULL; +} + +static struct listener_config_t *add_listener(int fd, struct sockaddr *addr, socklen_t addrlen, int is_global, int proxy_protocol) +{ + struct listener_config_t *listener = h2o_mem_alloc(sizeof(*listener)); + + memcpy(&listener->addr, addr, addrlen); + listener->fd = fd; + listener->addrlen = addrlen; + if (is_global) { + listener->hosts = NULL; + } else { + listener->hosts = h2o_mem_alloc(sizeof(listener->hosts[0])); + listener->hosts[0] = NULL; + } + memset(&listener->ssl, 0, sizeof(listener->ssl)); + listener->proxy_protocol = proxy_protocol; + + conf.listeners = h2o_mem_realloc(conf.listeners, sizeof(*conf.listeners) * (conf.num_listeners + 1)); + conf.listeners[conf.num_listeners++] = listener; + + return listener; +} + +static int find_listener_from_server_starter(struct sockaddr *addr) +{ + size_t i; + + assert(conf.server_starter.fds != NULL); + assert(conf.server_starter.num_fds != 0); + + for (i = 0; i != conf.server_starter.num_fds; ++i) { + struct sockaddr_storage sa; + socklen_t salen = sizeof(sa); + if (getsockname(conf.server_starter.fds[i], (void *)&sa, &salen) != 0) { + fprintf(stderr, "could not get the socket address of fd %d given as $SERVER_STARTER_PORT\n", + conf.server_starter.fds[i]); + exit(EX_CONFIG); + } + if (h2o_socket_compare_address((void *)&sa, addr) == 0) + goto Found; + } + /* not found */ + return -1; + +Found: + conf.server_starter.bound_fd_map[i] = 1; + return conf.server_starter.fds[i]; +} + +static int open_unix_listener(h2o_configurator_command_t *cmd, yoml_t *node, struct sockaddr_un *sa) +{ + struct stat st; + int fd = -1; + struct passwd *owner = NULL, pwbuf; + char pwbuf_buf[65536]; + unsigned mode = UINT_MAX; + yoml_t *t; + + /* obtain owner and permission */ + if ((t = yoml_get(node, "owner")) != NULL) { + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, t, "`owner` is not a scalar"); + goto ErrorExit; + } + if (getpwnam_r(t->data.scalar, &pwbuf, pwbuf_buf, sizeof(pwbuf_buf), &owner) != 0 || owner == NULL) { + h2o_configurator_errprintf(cmd, t, "failed to obtain uid of user:%s: %s", t->data.scalar, strerror(errno)); + goto ErrorExit; + } + } + if ((t = yoml_get(node, "permission")) != NULL) { + if (t->type != YOML_TYPE_SCALAR || sscanf(t->data.scalar, "%o", &mode) != 1) { + h2o_configurator_errprintf(cmd, t, "`permission` must be an octal number"); + goto ErrorExit; + } + } + + /* remove existing socket file as suggested in #45 */ + if (lstat(sa->sun_path, &st) == 0) { + if (S_ISSOCK(st.st_mode)) { + unlink(sa->sun_path); + } else { + h2o_configurator_errprintf(cmd, node, "path:%s already exists and is not an unix socket.", sa->sun_path); + goto ErrorExit; + } + } + + /* add new listener */ + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1 || bind(fd, (void *)sa, sizeof(*sa)) != 0 || listen(fd, H2O_SOMAXCONN) != 0) { + h2o_configurator_errprintf(NULL, node, "failed to listen to socket:%s: %s", sa->sun_path, strerror(errno)); + goto ErrorExit; + } + set_cloexec(fd); + + /* set file owner and permission */ + if (owner != NULL && chown(sa->sun_path, owner->pw_uid, owner->pw_gid) != 0) { + h2o_configurator_errprintf(NULL, node, "failed to chown socket:%s to %s: %s", sa->sun_path, owner->pw_name, + strerror(errno)); + goto ErrorExit; + } + if (mode != UINT_MAX && chmod(sa->sun_path, mode) != 0) { + h2o_configurator_errprintf(NULL, node, "failed to chmod socket:%s to %o: %s", sa->sun_path, mode, strerror(errno)); + goto ErrorExit; + } + + return fd; + +ErrorExit: + if (fd != -1) + close(fd); + return -1; +} + +static int open_tcp_listener(h2o_configurator_command_t *cmd, yoml_t *node, const char *hostname, const char *servname, int domain, + int type, int protocol, struct sockaddr *addr, socklen_t addrlen) +{ + int fd; + + if ((fd = socket(domain, type, protocol)) == -1) + goto Error; + set_cloexec(fd); + { /* set reuseaddr */ + int flag = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) != 0) + goto Error; + } +#ifdef TCP_DEFER_ACCEPT + { /* set TCP_DEFER_ACCEPT */ + int flag = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &flag, sizeof(flag)) != 0) + goto Error; + } +#endif +#ifdef IPV6_V6ONLY + /* set IPv6only */ + if (domain == AF_INET6) { + int flag = 1; + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &flag, sizeof(flag)) != 0) + goto Error; + } +#endif + if (bind(fd, addr, addrlen) != 0) + goto Error; + if (listen(fd, H2O_SOMAXCONN) != 0) + goto Error; + + /* set TCP_FASTOPEN; when tfo_queues is zero TFO is always disabled */ + if (conf.tfo_queues > 0) { +#ifdef TCP_FASTOPEN + int tfo_queues; +#ifdef __APPLE__ + /* In OS X, the option value for TCP_FASTOPEN must be 1 if is's enabled */ + tfo_queues = 1; +#else + tfo_queues = conf.tfo_queues; +#endif + if (setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, (const void *)&tfo_queues, sizeof(tfo_queues)) != 0) + fprintf(stderr, "[warning] failed to set TCP_FASTOPEN:%s\n", strerror(errno)); +#else + assert(!"conf.tfo_queues not zero on platform without TCP_FASTOPEN"); +#endif + } + + return fd; + +Error: + if (fd != -1) + close(fd); + h2o_configurator_errprintf(NULL, node, "failed to listen to port %s:%s: %s", hostname != NULL ? hostname : "ANY", servname, + strerror(errno)); + return -1; +} + +static int on_config_listen(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + const char *hostname = NULL, *servname = NULL, *type = "tcp"; + yoml_t *ssl_node = NULL; + int proxy_protocol = 0; + + /* fetch servname (and hostname) */ + switch (node->type) { + case YOML_TYPE_SCALAR: + servname = node->data.scalar; + break; + case YOML_TYPE_MAPPING: { + yoml_t *t; + if ((t = yoml_get(node, "host")) != NULL) { + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, t, "`host` is not a string"); + return -1; + } + hostname = t->data.scalar; + } + if ((t = yoml_get(node, "port")) == NULL) { + h2o_configurator_errprintf(cmd, node, "cannot find mandatory property `port`"); + return -1; + } + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, node, "`port` is not a string"); + return -1; + } + servname = t->data.scalar; + if ((t = yoml_get(node, "type")) != NULL) { + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, t, "`type` is not a string"); + return -1; + } + type = t->data.scalar; + } + if ((t = yoml_get(node, "ssl")) != NULL) + ssl_node = t; + if ((t = yoml_get(node, "proxy-protocol")) != NULL) { + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, node, "`proxy-protocol` must be a string"); + return -1; + } + if (strcasecmp(t->data.scalar, "ON") == 0) { + proxy_protocol = 1; + } else if (strcasecmp(t->data.scalar, "OFF") == 0) { + proxy_protocol = 0; + } else { + h2o_configurator_errprintf(cmd, node, "value of `proxy-protocol` must be either of: ON,OFF"); + return -1; + } + } + } break; + default: + h2o_configurator_errprintf(cmd, node, "value must be a string or a mapping (with keys: `port` and optionally `host`)"); + return -1; + } + + if (strcmp(type, "unix") == 0) { + + /* unix socket */ + struct sockaddr_un sa; + int listener_is_new; + struct listener_config_t *listener; + /* build sockaddr */ + memset(&sa, 0, sizeof(sa)); + if (strlen(servname) >= sizeof(sa.sun_path)) { + h2o_configurator_errprintf(cmd, node, "path:%s is too long as a unix socket name", servname); + return -1; + } + sa.sun_family = AF_UNIX; + strcpy(sa.sun_path, servname); + /* find existing listener or create a new one */ + listener_is_new = 0; + if ((listener = find_listener((void *)&sa, sizeof(sa))) == NULL) { + int fd = -1; + switch (conf.run_mode) { + case RUN_MODE_WORKER: + if (conf.server_starter.fds != NULL) { + if ((fd = find_listener_from_server_starter((void *)&sa)) == -1) { + h2o_configurator_errprintf(cmd, node, "unix socket:%s is not being bound to the server\n", sa.sun_path); + return -1; + } + } else { + if ((fd = open_unix_listener(cmd, node, &sa)) == -1) + return -1; + } + break; + default: + break; + } + listener = add_listener(fd, (struct sockaddr *)&sa, sizeof(sa), ctx->hostconf == NULL, proxy_protocol); + listener_is_new = 1; + } else if (listener->proxy_protocol != proxy_protocol) { + goto ProxyConflict; + } + if (listener_setup_ssl(cmd, ctx, node, ssl_node, listener, listener_is_new) != 0) + return -1; + if (listener->hosts != NULL && ctx->hostconf != NULL) + h2o_append_to_null_terminated_list((void *)&listener->hosts, ctx->hostconf); + + } else if (strcmp(type, "tcp") == 0) { + + /* TCP socket */ + struct addrinfo hints, *res, *ai; + int error; + /* call getaddrinfo */ + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV | AI_PASSIVE; + if ((error = getaddrinfo(hostname, servname, &hints, &res)) != 0) { + h2o_configurator_errprintf(cmd, node, "failed to resolve the listening address: %s", gai_strerror(error)); + return -1; + } else if (res == NULL) { + h2o_configurator_errprintf(cmd, node, "failed to resolve the listening address: getaddrinfo returned an empty list"); + return -1; + } + /* listen to the returned addresses */ + for (ai = res; ai != NULL; ai = ai->ai_next) { + struct listener_config_t *listener = find_listener(ai->ai_addr, ai->ai_addrlen); + int listener_is_new = 0; + if (listener == NULL) { + int fd = -1; + switch (conf.run_mode) { + case RUN_MODE_WORKER: + if (conf.server_starter.fds != NULL) { + if ((fd = find_listener_from_server_starter(ai->ai_addr)) == -1) { + h2o_configurator_errprintf(cmd, node, "tcp socket:%s:%s is not being bound to the server\n", hostname, + servname); + freeaddrinfo(res); + return -1; + } + } else { + if ((fd = open_tcp_listener(cmd, node, hostname, servname, ai->ai_family, ai->ai_socktype, ai->ai_protocol, + ai->ai_addr, ai->ai_addrlen)) == -1) { + freeaddrinfo(res); + return -1; + } + } + break; + default: + break; + } + listener = add_listener(fd, ai->ai_addr, ai->ai_addrlen, ctx->hostconf == NULL, proxy_protocol); + listener_is_new = 1; + } else if (listener->proxy_protocol != proxy_protocol) { + freeaddrinfo(res); + goto ProxyConflict; + } + if (listener_setup_ssl(cmd, ctx, node, ssl_node, listener, listener_is_new) != 0) { + freeaddrinfo(res); + return -1; + } + if (listener->hosts != NULL && ctx->hostconf != NULL) + h2o_append_to_null_terminated_list((void *)&listener->hosts, ctx->hostconf); + } + /* release res */ + freeaddrinfo(res); + + } else { + + h2o_configurator_errprintf(cmd, node, "unknown listen type: %s", type); + return -1; + } + + return 0; + +ProxyConflict: + h2o_configurator_errprintf(cmd, node, "`proxy-protocol` cannot be turned %s, already defined as opposite", + proxy_protocol ? "on" : "off"); + return -1; +} + +static int on_config_listen_enter(h2o_configurator_t *_configurator, h2o_configurator_context_t *ctx, yoml_t *node) +{ + return 0; +} + +static int on_config_listen_exit(h2o_configurator_t *_configurator, h2o_configurator_context_t *ctx, yoml_t *node) +{ + if (ctx->pathconf != NULL) { + /* skip */ + } else if (ctx->hostconf == NULL) { + /* at global level: bind all hostconfs to the global-level listeners */ + size_t i; + for (i = 0; i != conf.num_listeners; ++i) { + struct listener_config_t *listener = conf.listeners[i]; + if (listener->hosts == NULL) + listener->hosts = conf.globalconf.hosts; + } + } else if (ctx->pathconf == NULL) { + /* at host-level */ + if (conf.num_listeners == 0) { + h2o_configurator_errprintf( + NULL, node, + "mandatory configuration directive `listen` does not exist, neither at global level or at this host level"); + return -1; + } + } + + return 0; +} + +static int on_config_user(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + errno = 0; + if (getpwnam(node->data.scalar) == NULL) { + if (errno == 0) { + h2o_configurator_errprintf(cmd, node, "user:%s does not exist", node->data.scalar); + } else { + perror("getpwnam"); + } + return -1; + } + ctx->globalconf->user = h2o_strdup(NULL, node->data.scalar, SIZE_MAX).base; + return 0; +} + +static int on_config_pid_file(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + conf.pid_file = h2o_strdup(NULL, node->data.scalar, SIZE_MAX).base; + return 0; +} + +static int on_config_error_log(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + conf.error_log = h2o_strdup(NULL, node->data.scalar, SIZE_MAX).base; + return 0; +} + +static int on_config_max_connections(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + return h2o_configurator_scanf(cmd, node, "%d", &conf.max_connections); +} + +static int on_config_num_threads(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + if (h2o_configurator_scanf(cmd, node, "%zu", &conf.num_threads) != 0) + return -1; + if (conf.num_threads == 0) { + h2o_configurator_errprintf(cmd, node, "num-threads must be >=1"); + return -1; + } + return 0; +} + +static int on_config_num_name_resolution_threads(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + if (h2o_configurator_scanf(cmd, node, "%zu", &h2o_hostinfo_max_threads) != 0) + return -1; + if (h2o_hostinfo_max_threads == 0) { + h2o_configurator_errprintf(cmd, node, "num-name-resolution-threads must be >=1"); + return -1; + } + return 0; +} + +static int on_config_tcp_fastopen(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + if (h2o_configurator_scanf(cmd, node, "%d", &conf.tfo_queues) != 0) + return -1; +#ifndef TCP_FASTOPEN + if (conf.tfo_queues != 0) { + h2o_configurator_errprintf(cmd, node, "[warning] ignoring the value; the platform does not support TCP_FASTOPEN"); + conf.tfo_queues = 0; + } +#endif + return 0; +} + +static int on_config_num_ocsp_updaters(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + ssize_t n; + if (h2o_configurator_scanf(cmd, node, "%zd", &n) != 0) + return -1; + if (n <= 0) { + h2o_configurator_errprintf(cmd, node, "num-ocsp-updaters must be >=1"); + return -1; + } + h2o_sem_set_capacity(&ocsp_updater_semaphore, n); + return 0; +} + +static int on_config_temp_buffer_path(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + char buf[sizeof(h2o_socket_buffer_mmap_settings.fn_template)]; + + int len = snprintf(buf, sizeof(buf), "%s%s", node->data.scalar, strrchr(h2o_socket_buffer_mmap_settings.fn_template, '/')); + if (len >= sizeof(buf)) { + h2o_configurator_errprintf(cmd, node, "path is too long"); + return -1; + } + strcpy(h2o_socket_buffer_mmap_settings.fn_template, buf); + + return 0; +} + +static int on_config_crash_handler(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + conf.crash_handler = h2o_strdup(NULL, node->data.scalar, SIZE_MAX).base; + return 0; +} + +static int on_config_crash_handler_wait_pipe_close(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + ssize_t v; + + if ((v = h2o_configurator_get_one_of(cmd, node, "OFF,ON")) == -1) + return -1; + + conf.crash_handler_wait_pipe_close = (int)v; + return 0; +} + +static yoml_t *load_config(yoml_parse_args_t *parse_args, yoml_t *source) +{ + FILE *fp; + yaml_parser_t parser; + yoml_t *yoml; + + if ((fp = fopen(parse_args->filename, "rb")) == NULL) { + fprintf(stderr, "could not open configuration file %s: %s\n", parse_args->filename, strerror(errno)); + return NULL; + } + + yaml_parser_initialize(&parser); + yaml_parser_set_input_file(&parser, fp); + + yoml = yoml_parse_document(&parser, NULL, parse_args); + + if (yoml == NULL) { + fprintf(stderr, "failed to parse configuration file %s line %d", parse_args->filename, (int)parser.problem_mark.line + 1); + if (source != NULL) { + fprintf(stderr, " (included from file %s line %d)", source->filename, (int)source->line + 1); + } + fprintf(stderr, ": %s\n", parser.problem); + } + + yaml_parser_delete(&parser); + + fclose(fp); + + return yoml; +} + +static yoml_t *resolve_tag(const char *tag, yoml_t *node, void *cb_arg); +static yoml_t *resolve_file_tag(yoml_t *node, resolve_tag_arg_t *arg) +{ + size_t i; + yoml_t *loaded; + + if (node->type != YOML_TYPE_SCALAR) { + fprintf(stderr, "value of the !file node must be a scalar"); + return NULL; + } + + char *filename = node->data.scalar; + + /* check cache */ + for (i = 0; i != arg->node_cache.size; ++i) { + resolve_tag_node_cache_entry_t *cached = arg->node_cache.entries + i; + if (strcmp(filename, cached->filename.base) == 0) { + ++cached->node->_refcnt; + return cached->node; + } + } + + yoml_parse_args_t parse_args = { + filename, /* filename */ + NULL, /* mem_set */ + {resolve_tag, arg} /* resolve_tag */ + }; + loaded = load_config(&parse_args, node); + + if (loaded != NULL) { + /* cache newly loaded node */ + h2o_vector_reserve(NULL, &arg->node_cache, arg->node_cache.size + 1); + resolve_tag_node_cache_entry_t entry = {h2o_strdup(NULL, filename, SIZE_MAX), loaded}; + arg->node_cache.entries[arg->node_cache.size++] = entry; + ++loaded->_refcnt; + } + + return loaded; +} + +static yoml_t *resolve_tag(const char *tag, yoml_t *node, void *cb_arg) +{ + resolve_tag_arg_t *arg = (resolve_tag_arg_t *)cb_arg; + + if (strcmp(tag, "!file") == 0) { + return resolve_file_tag(node, arg); + } + + /* otherwise, return the node itself */ + ++node->_refcnt; + return node; +} + +static void dispose_resolve_tag_arg(resolve_tag_arg_t *arg) +{ + size_t i; + for (i = 0; i != arg->node_cache.size; ++i) { + resolve_tag_node_cache_entry_t *cached = arg->node_cache.entries + i; + free(cached->filename.base); + yoml_free(cached->node, NULL); + } + free(arg->node_cache.entries); +} + +static void notify_all_threads(void) +{ + unsigned i; + for (i = 0; i != conf.num_threads; ++i) + h2o_multithread_send_message(&conf.threads[i].server_notifications, NULL); +} + +static void on_sigterm(int signo) +{ + conf.shutdown_requested = 1; + if (!h2o_barrier_done(&conf.startup_sync_barrier)) { + /* initialization hasn't completed yet, exit right away */ + exit(0); + } + notify_all_threads(); +} + +#ifdef __GLIBC__ +static int popen_crash_handler(void) +{ + char *cmd_fullpath = h2o_configurator_get_cmd_path(conf.crash_handler), *argv[] = {cmd_fullpath, NULL}; + int pipefds[2]; + + /* create pipe */ + if (pipe(pipefds) != 0) { + perror("pipe failed"); + return -1; + } + if (fcntl(pipefds[1], F_SETFD, FD_CLOEXEC) == -1) { + perror("failed to set FD_CLOEXEC on pipefds[1]"); + return -1; + } + /* spawn the logger */ + int mapped_fds[] = {pipefds[0], 0, /* output of the pipe is connected to STDIN of the spawned process */ + 2, 1, /* STDOUT of the spawned process in connected to STDERR of h2o */ + -1}; + if (h2o_spawnp(cmd_fullpath, argv, mapped_fds, 0) == -1) { + /* silently ignore error */ + close(pipefds[0]); + close(pipefds[1]); + return -1; + } + /* do the rest, and return the fd */ + close(pipefds[0]); + return pipefds[1]; +} + +static int crash_handler_fd = -1; + +static void on_sigfatal(int signo) +{ + fprintf(stderr, "received fatal signal %d\n", signo); + + h2o_set_signal_handler(signo, SIG_DFL); + + void *frames[128]; + int framecnt = backtrace(frames, sizeof(frames) / sizeof(frames[0])); + backtrace_symbols_fd(frames, framecnt, crash_handler_fd); + + if (conf.crash_handler_wait_pipe_close) { + struct pollfd pfd[1]; + pfd[0].fd = crash_handler_fd; + pfd[0].events = POLLERR | POLLHUP; + while (poll(pfd, 1, -1) == -1 && errno == EINTR) + ; + } + + raise(signo); +} +#endif + +static void setup_signal_handlers(void) +{ + h2o_set_signal_handler(SIGTERM, on_sigterm); + h2o_set_signal_handler(SIGPIPE, SIG_IGN); +#ifdef __GLIBC__ + if ((crash_handler_fd = popen_crash_handler()) == -1) + crash_handler_fd = 2; + h2o_set_signal_handler(SIGABRT, on_sigfatal); + h2o_set_signal_handler(SIGBUS, on_sigfatal); + h2o_set_signal_handler(SIGFPE, on_sigfatal); + h2o_set_signal_handler(SIGILL, on_sigfatal); + h2o_set_signal_handler(SIGSEGV, on_sigfatal); +#endif +} + +static int num_connections(int delta) +{ + return __sync_fetch_and_add(&conf.state._num_connections, delta); +} + +static unsigned long num_sessions(int delta) +{ + return __sync_fetch_and_add(&conf.state._num_sessions, delta); +} + +static void on_socketclose(void *data) +{ + int prev_num_connections = num_connections(-1); + + if (prev_num_connections == conf.max_connections) { + /* ready to accept new connections. wake up all the threads! */ + notify_all_threads(); + } +} + +static void on_accept(h2o_socket_t *listener, const char *err) +{ + struct listener_ctx_t *ctx = listener->data; + size_t num_accepts = conf.max_connections / 16 / conf.num_threads; + if (num_accepts < 8) + num_accepts = 8; + + if (err != NULL) { + return; + } + + do { + h2o_socket_t *sock; + if (num_connections(0) >= conf.max_connections) { + /* The accepting socket is disactivated before entering the next in `run_loop`. + * Note: it is possible that the server would accept at most `max_connections + num_threads` connections, since the + * server does not check if the number of connections has exceeded _after_ epoll notifies of a new connection _but_ + * _before_ calling `accept`. In other words t/40max-connections.t may fail. + */ + break; + } + if ((sock = h2o_evloop_socket_accept(listener)) == NULL) { + break; + } + num_connections(1); + num_sessions(1); + + sock->on_close.cb = on_socketclose; + sock->on_close.data = ctx->accept_ctx.ctx; + + h2o_accept(&ctx->accept_ctx, sock); + + } while (--num_accepts != 0); +} + +static void update_listener_state(struct listener_ctx_t *listeners) +{ + size_t i; + + if (num_connections(0) < conf.max_connections) { + for (i = 0; i != conf.num_listeners; ++i) { + if (!h2o_socket_is_reading(listeners[i].sock)) + h2o_socket_read_start(listeners[i].sock, on_accept); + } + } else { + for (i = 0; i != conf.num_listeners; ++i) { + if (h2o_socket_is_reading(listeners[i].sock)) + h2o_socket_read_stop(listeners[i].sock); + } + } +} + +static void on_server_notification(h2o_multithread_receiver_t *receiver, h2o_linklist_t *messages) +{ + /* the notification is used only for exitting h2o_evloop_run; actual changes are done in the main loop of run_loop */ + + while (!h2o_linklist_is_empty(messages)) { + h2o_multithread_message_t *message = H2O_STRUCT_FROM_MEMBER(h2o_multithread_message_t, link, messages->next); + h2o_linklist_unlink(&message->link); + free(message); + } +} + +H2O_NORETURN static void *run_loop(void *_thread_index) +{ + size_t thread_index = (size_t)_thread_index; + struct listener_ctx_t *listeners = alloca(sizeof(*listeners) * conf.num_listeners); + size_t i; + + h2o_context_init(&conf.threads[thread_index].ctx, h2o_evloop_create(), &conf.globalconf); + h2o_multithread_register_receiver(conf.threads[thread_index].ctx.queue, &conf.threads[thread_index].server_notifications, + on_server_notification); + h2o_multithread_register_receiver(conf.threads[thread_index].ctx.queue, &conf.threads[thread_index].memcached, + h2o_memcached_receiver); + conf.threads[thread_index].tid = pthread_self(); + + /* setup listeners */ + for (i = 0; i != conf.num_listeners; ++i) { + struct listener_config_t *listener_config = conf.listeners[i]; + int fd; + /* dup the listener fd for other threads than the main thread */ + if (thread_index == 0) { + fd = listener_config->fd; + } else { + if ((fd = dup(listener_config->fd)) == -1) { + perror("failed to dup listening socket"); + abort(); + } + set_cloexec(fd); + } + memset(listeners + i, 0, sizeof(listeners[i])); + listeners[i].accept_ctx.ctx = &conf.threads[thread_index].ctx; + listeners[i].accept_ctx.hosts = listener_config->hosts; + if (listener_config->ssl.size != 0) + listeners[i].accept_ctx.ssl_ctx = listener_config->ssl.entries[0]->ctx; + listeners[i].accept_ctx.expect_proxy_line = listener_config->proxy_protocol; + listeners[i].accept_ctx.libmemcached_receiver = &conf.threads[thread_index].memcached; + listeners[i].sock = h2o_evloop_socket_create(conf.threads[thread_index].ctx.loop, fd, H2O_SOCKET_FLAG_DONT_READ); + listeners[i].sock->data = listeners + i; + } + /* and start listening */ + update_listener_state(listeners); + + /* make sure all threads are initialized before starting to serve requests */ + h2o_barrier_wait(&conf.startup_sync_barrier); + + /* the main loop */ + while (1) { + if (conf.shutdown_requested) + break; + update_listener_state(listeners); + /* run the loop once */ + h2o_evloop_run(conf.threads[thread_index].ctx.loop, INT32_MAX); + h2o_filecache_clear(conf.threads[thread_index].ctx.filecache); + } + + if (thread_index == 0) + fprintf(stderr, "received SIGTERM, gracefully shutting down\n"); + + /* shutdown requested, unregister, close the listeners and notify the protocol handlers */ + for (i = 0; i != conf.num_listeners; ++i) + h2o_socket_read_stop(listeners[i].sock); + h2o_evloop_run(conf.threads[thread_index].ctx.loop, 0); + for (i = 0; i != conf.num_listeners; ++i) { + h2o_socket_close(listeners[i].sock); + listeners[i].sock = NULL; + } + h2o_context_request_shutdown(&conf.threads[thread_index].ctx); + + /* wait until all the connection gets closed */ + while (num_connections(0) != 0) + h2o_evloop_run(conf.threads[thread_index].ctx.loop, INT32_MAX); + + /* the process that detects num_connections becoming zero performs the last cleanup */ + if (conf.pid_file != NULL) + unlink(conf.pid_file); + _exit(0); +} + +static char **build_server_starter_argv(const char *h2o_cmd, const char *config_file) +{ + H2O_VECTOR(char *) args = {NULL}; + size_t i; + + h2o_vector_reserve(NULL, &args, 1); + args.entries[args.size++] = h2o_configurator_get_cmd_path("share/h2o/start_server"); + + /* error-log and pid-file are the directives that are handled by server-starter */ + if (conf.pid_file != NULL) { + h2o_vector_reserve(NULL, &args, args.size + 1); + args.entries[args.size++] = + h2o_concat(NULL, h2o_iovec_init(H2O_STRLIT("--pid-file=")), h2o_iovec_init(conf.pid_file, strlen(conf.pid_file))).base; + } + if (conf.error_log != NULL) { + h2o_vector_reserve(NULL, &args, args.size + 1); + args.entries[args.size++] = + h2o_concat(NULL, h2o_iovec_init(H2O_STRLIT("--log-file=")), h2o_iovec_init(conf.error_log, strlen(conf.error_log))) + .base; + } + + switch (conf.run_mode) { + case RUN_MODE_DAEMON: + h2o_vector_reserve(NULL, &args, args.size + 1); + args.entries[args.size++] = "--daemonize"; + break; + default: + break; + } + + for (i = 0; i != conf.num_listeners; ++i) { + char *newarg; + switch (conf.listeners[i]->addr.ss_family) { + default: { + char host[NI_MAXHOST], serv[NI_MAXSERV]; + int err; + if ((err = getnameinfo((void *)&conf.listeners[i]->addr, conf.listeners[i]->addrlen, host, sizeof(host), serv, + sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV)) != 0) { + fprintf(stderr, "failed to stringify the address of %zu-th listen directive:%s\n", i, gai_strerror(err)); + exit(EX_OSERR); + } + newarg = h2o_mem_alloc(sizeof("--port=[]:") + strlen(host) + strlen(serv)); + if (strchr(host, ':') != NULL) { + sprintf(newarg, "--port=[%s]:%s", host, serv); + } else { + sprintf(newarg, "--port=%s:%s", host, serv); + } + } break; + case AF_UNIX: { + struct sockaddr_un *sa = (void *)&conf.listeners[i]->addr; + newarg = h2o_mem_alloc(sizeof("--path=") + strlen(sa->sun_path)); + sprintf(newarg, "--path=%s", sa->sun_path); + } break; + } + h2o_vector_reserve(NULL, &args, args.size + 1); + args.entries[args.size++] = newarg; + } + + h2o_vector_reserve(NULL, &args, args.size + 5); + args.entries[args.size++] = "--"; + args.entries[args.size++] = (char *)h2o_cmd; + args.entries[args.size++] = "-c"; + args.entries[args.size++] = (char *)config_file; + args.entries[args.size] = NULL; + + return args.entries; +} + +static int run_using_server_starter(const char *h2o_cmd, const char *config_file) +{ + char **args = build_server_starter_argv(h2o_cmd, config_file); + setenv("H2O_VIA_MASTER", "", 1); + execvp(args[0], args); + fprintf(stderr, "failed to spawn %s:%s\n", args[0], strerror(errno)); + return EX_CONFIG; +} + +/* make jemalloc linkage optional by marking the functions as 'weak', + * since upstream doesn't rely on it. */ +struct extra_status_jemalloc_cb_arg { + h2o_iovec_t outbuf; + int err; + size_t written; +}; + +#if JEMALLOC_STATS == 1 +static void extra_status_jemalloc_cb(void *ctx, const char *stats) +{ + size_t cur_len; + struct extra_status_jemalloc_cb_arg *out = ctx; + h2o_iovec_t outbuf = out->outbuf; + int i; + + if (out->written >= out->outbuf.len || out->err) { + return; + } + cur_len = out->written; + + i = 0; + while (cur_len < outbuf.len && stats[i]) { + switch (stats[i]) { +#define JSON_ESCAPE(x, y) \ + case x: \ + outbuf.base[cur_len++] = '\\'; \ + if (cur_len >= outbuf.len) { \ + goto err; \ + } \ + outbuf.base[cur_len] = y; \ + break; + JSON_ESCAPE('\b', 'b'); + JSON_ESCAPE('\f', 'f'); + JSON_ESCAPE('\n', 'n'); + JSON_ESCAPE('\r', 'r') + JSON_ESCAPE('\t', 't'); + JSON_ESCAPE('/', '/'); + JSON_ESCAPE('"', '"'); + JSON_ESCAPE('\\', '\\'); +#undef JSON_ESCAPE + default: + outbuf.base[cur_len] = stats[i]; + } + i++; + cur_len++; + } + if (cur_len < outbuf.len) { + out->written = cur_len; + return; + } + +err: + out->err = 1; + return; +} +#endif + +static h2o_iovec_t on_extra_status(void *unused, h2o_globalconf_t *_conf, h2o_req_t *req) +{ +#define BUFSIZE (16 * 1024) + h2o_iovec_t ret; + char current_time[H2O_TIMESTR_LOG_LEN + 1], restart_time[H2O_TIMESTR_LOG_LEN + 1]; + const char *generation; + time_t now = time(NULL); + + h2o_time2str_log(current_time, now); + h2o_time2str_log(restart_time, conf.launch_time); + if ((generation = getenv("SERVER_STARTER_GENERATION")) == NULL) + generation = "null"; + + ret.base = h2o_mem_alloc_pool(&req->pool, BUFSIZE); + ret.len = snprintf(ret.base, BUFSIZE, ",\n" + " \"server-version\": \"" H2O_VERSION "\",\n" + " \"openssl-version\": \"%s\",\n" + " \"current-time\": \"%s\",\n" + " \"restart-time\": \"%s\",\n" + " \"uptime\": %" PRIu64 ",\n" + " \"generation\": %s,\n" + " \"connections\": %d,\n" + " \"max-connections\": %d,\n" + " \"listeners\": %zu,\n" + " \"worker-threads\": %zu,\n" + " \"num-sessions\": %lu", + SSLeay_version(SSLEAY_VERSION), current_time, restart_time, (uint64_t)(now - conf.launch_time), generation, + num_connections(0), conf.max_connections, conf.num_listeners, conf.num_threads, num_sessions(0)); + assert(ret.len < BUFSIZE); + +#if JEMALLOC_STATS == 1 + struct extra_status_jemalloc_cb_arg arg; + size_t sz, allocated, active, metadata, resident, mapped; + uint64_t epoch = 1; + /* internal jemalloc interface */ + void malloc_stats_print(void (*write_cb)(void *, const char *), void *cbopaque, const char *opts); + int mallctl(const char *name, void *oldp, size_t *oldlenp, void *newp, size_t newlen); + + arg.outbuf = h2o_iovec_init(alloca(BUFSIZE - ret.len), BUFSIZE - ret.len); + arg.err = 0; + arg.written = snprintf(arg.outbuf.base, arg.outbuf.len, ",\n" + " \"jemalloc\": {\n" + " \"jemalloc-raw\": \""); + malloc_stats_print(extra_status_jemalloc_cb, &arg, "ga" /* omit general info, only aggregated stats */); + + if (arg.err || arg.written + 1 >= arg.outbuf.len) { + goto jemalloc_err; + } + + /* terminate the jemalloc-raw json string */ + arg.written += snprintf(&arg.outbuf.base[arg.written], arg.outbuf.len - arg.written, "\""); + if (arg.written + 1 >= arg.outbuf.len) { + goto jemalloc_err; + } + + sz = sizeof(epoch); + mallctl("epoch", &epoch, &sz, &epoch, sz); + + sz = sizeof(size_t); + if (!mallctl("stats.allocated", &allocated, &sz, NULL, 0) && !mallctl("stats.active", &active, &sz, NULL, 0) && + !mallctl("stats.metadata", &metadata, &sz, NULL, 0) && !mallctl("stats.resident", &resident, &sz, NULL, 0) && + !mallctl("stats.mapped", &mapped, &sz, NULL, 0)) { + arg.written += snprintf(&arg.outbuf.base[arg.written], arg.outbuf.len - arg.written, ",\n" + " \"allocated\": %zu,\n" + " \"active\": %zu,\n" + " \"metadata\": %zu,\n" + " \"resident\": %zu,\n" + " \"mapped\": %zu }", + allocated, active, metadata, resident, mapped); + } + if (arg.written + 1 >= arg.outbuf.len) { + goto jemalloc_err; + } + + strncpy(&ret.base[ret.len], arg.outbuf.base, arg.written); + ret.base[ret.len + arg.written] = '\0'; + ret.len += arg.written; + return ret; + +jemalloc_err: + /* couldn't fit the jemalloc output, exiting */ + ret.base[ret.len] = '\0'; + +#endif /* JEMALLOC_STATS == 1 */ + + return ret; +#undef BUFSIZE +} + +static void setup_configurators(void) +{ + h2o_config_init(&conf.globalconf); + + /* let the default setuid user be "nobody", if run as root */ + if (getuid() == 0 && getpwnam("nobody") != NULL) + conf.globalconf.user = "nobody"; + + { + h2o_configurator_t *c = h2o_configurator_create(&conf.globalconf, sizeof(*c)); + c->enter = on_config_listen_enter; + c->exit = on_config_listen_exit; + h2o_configurator_define_command(c, "listen", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_HOST, on_config_listen); + } + + { + h2o_configurator_t *c = h2o_configurator_create(&conf.globalconf, sizeof(*c)); + h2o_configurator_define_command(c, "user", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_user); + h2o_configurator_define_command(c, "pid-file", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_pid_file); + h2o_configurator_define_command(c, "error-log", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_error_log); + h2o_configurator_define_command(c, "max-connections", H2O_CONFIGURATOR_FLAG_GLOBAL, on_config_max_connections); + h2o_configurator_define_command(c, "num-threads", H2O_CONFIGURATOR_FLAG_GLOBAL, on_config_num_threads); + h2o_configurator_define_command(c, "num-name-resolution-threads", H2O_CONFIGURATOR_FLAG_GLOBAL, + on_config_num_name_resolution_threads); + h2o_configurator_define_command(c, "tcp-fastopen", H2O_CONFIGURATOR_FLAG_GLOBAL, on_config_tcp_fastopen); + h2o_configurator_define_command(c, "ssl-session-resumption", + H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_MAPPING, + ssl_session_resumption_on_config); + h2o_configurator_define_command(c, "num-ocsp-updaters", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_num_ocsp_updaters); + h2o_configurator_define_command(c, "temp-buffer-path", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_temp_buffer_path); + h2o_configurator_define_command(c, "crash-handler", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_crash_handler); + h2o_configurator_define_command(c, "crash-handler.wait-pipe-close", + H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, + on_config_crash_handler_wait_pipe_close); + } + + h2o_access_log_register_configurator(&conf.globalconf); + h2o_compress_register_configurator(&conf.globalconf); + h2o_expires_register_configurator(&conf.globalconf); + h2o_errordoc_register_configurator(&conf.globalconf); + h2o_fastcgi_register_configurator(&conf.globalconf); + h2o_file_register_configurator(&conf.globalconf); + h2o_throttle_resp_register_configurator(&conf.globalconf); + h2o_headers_register_configurator(&conf.globalconf); + h2o_proxy_register_configurator(&conf.globalconf); + h2o_reproxy_register_configurator(&conf.globalconf); + h2o_redirect_register_configurator(&conf.globalconf); + h2o_status_register_configurator(&conf.globalconf); + h2o_http2_debug_state_register_configurator(&conf.globalconf); +#if H2O_USE_MRUBY + h2o_mruby_register_configurator(&conf.globalconf); +#endif + + h2o_config_register_simple_status_handler(&conf.globalconf, (h2o_iovec_t){H2O_STRLIT("main")}, on_extra_status); +} + +int main(int argc, char **argv) +{ + const char *cmd = argv[0], *opt_config_file = H2O_TO_STR(H2O_CONFIG_PATH); + int error_log_fd = -1; + + conf.num_threads = h2o_numproc(); + conf.tfo_queues = H2O_DEFAULT_LENGTH_TCP_FASTOPEN_QUEUE; + conf.launch_time = time(NULL); + + h2o_hostinfo_max_threads = H2O_DEFAULT_NUM_NAME_RESOLUTION_THREADS; + + h2o_sem_init(&ocsp_updater_semaphore, H2O_DEFAULT_OCSP_UPDATER_MAX_THREADS); + + init_openssl(); + setup_configurators(); + + { /* parse options */ + int ch; + static struct option longopts[] = {{"conf", required_argument, NULL, 'c'}, {"mode", required_argument, NULL, 'm'}, + {"test", no_argument, NULL, 't'}, {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, {NULL}}; + while ((ch = getopt_long(argc, argv, "c:m:tvh", longopts, NULL)) != -1) { + switch (ch) { + case 'c': + opt_config_file = optarg; + break; + case 'm': + if (strcmp(optarg, "worker") == 0) { + conf.run_mode = RUN_MODE_WORKER; + } else if (strcmp(optarg, "master") == 0) { + conf.run_mode = RUN_MODE_MASTER; + } else if (strcmp(optarg, "daemon") == 0) { + conf.run_mode = RUN_MODE_DAEMON; + } else if (strcmp(optarg, "test") == 0) { + conf.run_mode = RUN_MODE_TEST; + } else { + fprintf(stderr, "unknown mode:%s\n", optarg); + } + switch (conf.run_mode) { + case RUN_MODE_MASTER: + case RUN_MODE_DAEMON: + if (getenv("SERVER_STARTER_PORT") != NULL) { + fprintf(stderr, "refusing to start in `%s` mode, environment variable SERVER_STARTER_PORT is already set\n", + optarg); + exit(EX_SOFTWARE); + } + break; + default: + break; + } + break; + case 't': + conf.run_mode = RUN_MODE_TEST; + break; + case 'v': + printf("h2o version " H2O_VERSION "\n"); + printf("OpenSSL: %s\n", SSLeay_version(SSLEAY_VERSION)); +#if H2O_USE_MRUBY + printf( + "mruby: YES\n"); /* TODO determine the way to obtain the version of mruby (that is being linked dynamically) */ +#endif + exit(0); + case 'h': + printf("h2o version " H2O_VERSION "\n" + "\n" + "Usage:\n" + " h2o [options]\n" + "\n" + "Options:\n" + " -c, --conf FILE configuration file (default: %s)\n" + " -m, --mode specifies one of the following mode\n" + " - worker: invoked process handles incoming connections\n" + " (default)\n" + " - daemon: spawns a master process and exits. `error-log`\n" + " must be configured when using this mode, as all\n" + " the errors are logged to the file instead of\n" + " being emitted to STDERR\n" + " - master: invoked process becomes a master process (using\n" + " the `share/h2o/start_server` command) and spawns\n" + " a worker process for handling incoming\n" + " connections. Users may send SIGHUP to the master\n" + " process to reconfigure or upgrade the server.\n" + " - test: tests the configuration and exits\n" + " -t, --test synonym of `--mode=test`\n" + " -v, --version prints the version number\n" + " -h, --help print this help\n" + "\n" + "Please refer to the documentation under `share/doc/h2o` (or available online at\n" + "http://h2o.examp1e.net/) for how to configure the server.\n" + "\n", + H2O_TO_STR(H2O_CONFIG_PATH)); + exit(0); + break; + case ':': + case '?': + exit(EX_CONFIG); + default: + assert(0); + break; + } + } + argc -= optind; + argv += optind; + } + + /* setup conf.server_starter */ + if ((conf.server_starter.num_fds = h2o_server_starter_get_fds(&conf.server_starter.fds)) == SIZE_MAX) + exit(EX_CONFIG); + if (conf.server_starter.fds != 0) { + size_t i; + for (i = 0; i != conf.server_starter.num_fds; ++i) + set_cloexec(conf.server_starter.fds[i]); + conf.server_starter.bound_fd_map = alloca(conf.server_starter.num_fds); + memset(conf.server_starter.bound_fd_map, 0, conf.server_starter.num_fds); + conf.server_starter.env_var = getenv("SERVER_STARTER_PORT"); + } + unsetenv("SERVER_STARTER_PORT"); + + { /* configure */ + yoml_t *yoml; + resolve_tag_arg_t resolve_tag_arg = {{NULL}}; + yoml_parse_args_t parse_args = { + opt_config_file, /* filename */ + NULL, /* mem_set */ + {resolve_tag, &resolve_tag_arg} /* resolve_tag */ + }; + if ((yoml = load_config(&parse_args, NULL)) == NULL) + exit(EX_CONFIG); + if (h2o_configurator_apply(&conf.globalconf, yoml, conf.run_mode != RUN_MODE_WORKER) != 0) + exit(EX_CONFIG); + + dispose_resolve_tag_arg(&resolve_tag_arg); + yoml_free(yoml, NULL); + } + /* calculate defaults (note: open file cached is purged once every loop) */ + conf.globalconf.filecache.capacity = conf.globalconf.http2.max_concurrent_requests_per_connection * 2; + + /* check if all the fds passed in by server::starter were bound */ + if (conf.server_starter.fds != NULL) { + size_t i; + int all_were_bound = 1; + for (i = 0; i != conf.server_starter.num_fds; ++i) { + if (!conf.server_starter.bound_fd_map[i]) { + fprintf(stderr, "no configuration found for fd:%d passed in by $SERVER_STARTER_PORT\n", conf.server_starter.fds[i]); + all_were_bound = 0; + } + } + if (!all_were_bound) { + fprintf(stderr, "note: $SERVER_STARTER_PORT was \"%s\"\n", conf.server_starter.env_var); + return EX_CONFIG; + } + } + + h2o_srand(); + /* handle run_mode == MASTER|TEST */ + switch (conf.run_mode) { + case RUN_MODE_WORKER: + break; + case RUN_MODE_DAEMON: + if (conf.error_log == NULL) { + fprintf(stderr, "to run in `daemon` mode, `error-log` must be specified in the configuration file\n"); + return EX_CONFIG; + } + return run_using_server_starter(cmd, opt_config_file); + case RUN_MODE_MASTER: + return run_using_server_starter(cmd, opt_config_file); + case RUN_MODE_TEST: + printf("configuration OK\n"); + return 0; + } + + if (getenv("H2O_VIA_MASTER") != NULL) { + /* pid_file and error_log are the directives that are handled by the master process (invoking start_server) */ + conf.pid_file = NULL; + conf.error_log = NULL; + } + + { /* raise RLIMIT_NOFILE */ + struct rlimit limit; + if (getrlimit(RLIMIT_NOFILE, &limit) == 0) { + limit.rlim_cur = limit.rlim_max; + if (setrlimit(RLIMIT_NOFILE, &limit) == 0 +#ifdef __APPLE__ + || (limit.rlim_cur = OPEN_MAX, setrlimit(RLIMIT_NOFILE, &limit)) == 0 +#endif + ) { + fprintf(stderr, "[INFO] raised RLIMIT_NOFILE to %d\n", (int)limit.rlim_cur); + } + } + } + + setup_signal_handlers(); + + /* open the log file to redirect STDIN/STDERR to, before calling setuid */ + if (conf.error_log != NULL) { + if ((error_log_fd = h2o_access_log_open_log(conf.error_log)) == -1) + return EX_CONFIG; + } + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IOLBF, 0); + + /* setuid */ + if (conf.globalconf.user != NULL) { + if (h2o_setuidgid(conf.globalconf.user) != 0) { + fprintf(stderr, "failed to change the running user (are you sure you are running as root?)\n"); + return EX_OSERR; + } + if (neverbleed != NULL && neverbleed_setuidgid(neverbleed, conf.globalconf.user, 1) != 0) { + fprintf(stderr, "failed to change the running user of neverbleed daemon\n"); + return EX_OSERR; + } + } else { + if (getuid() == 0) { + fprintf(stderr, "refusing to run as root (and failed to switch to `nobody`); you can use the `user` directive to set " + "the running user\n"); + return EX_CONFIG; + } + } + + /* pid file must be written after setuid, since we need to remove it */ + if (conf.pid_file != NULL) { + FILE *fp = fopen(conf.pid_file, "wt"); + if (fp == NULL) { + fprintf(stderr, "failed to open pid file:%s:%s\n", conf.pid_file, strerror(errno)); + return EX_OSERR; + } + fprintf(fp, "%d\n", (int)getpid()); + fclose(fp); + } + + { /* initialize SSL_CTXs for session resumption and ticket-based resumption (also starts memcached client threads for the + purpose) */ + size_t i, j; + H2O_VECTOR(SSL_CTX *) ssl_contexts = {NULL}; + for (i = 0; i != conf.num_listeners; ++i) { + for (j = 0; j != conf.listeners[i]->ssl.size; ++j) { + h2o_vector_reserve(NULL, &ssl_contexts, ssl_contexts.size + 1); + ssl_contexts.entries[ssl_contexts.size++] = conf.listeners[i]->ssl.entries[j]->ctx; + } + } + ssl_setup_session_resumption(ssl_contexts.entries, ssl_contexts.size); + free(ssl_contexts.entries); + } + + /* all setup should be complete by now */ + + /* replace STDIN to an closed pipe */ + { + int fds[2]; + if (pipe(fds) != 0) { + perror("pipe failed"); + return EX_OSERR; + } + close(fds[1]); + dup2(fds[0], 0); + close(fds[0]); + } + + /* redirect STDOUT and STDERR to error_log (if specified) */ + if (error_log_fd != -1) { + if (dup2(error_log_fd, 1) == -1 || dup2(error_log_fd, 2) == -1) { + perror("dup(2) failed"); + return EX_OSERR; + } + close(error_log_fd); + error_log_fd = -1; + } + + fprintf(stderr, "h2o server (pid:%d) is ready to serve requests\n", (int)getpid()); + + assert(conf.num_threads != 0); + + /* start the threads */ + conf.threads = alloca(sizeof(conf.threads[0]) * conf.num_threads); + h2o_barrier_init(&conf.startup_sync_barrier, conf.num_threads); + size_t i; + for (i = 1; i != conf.num_threads; ++i) { + pthread_t tid; + h2o_multithread_create_thread(&tid, NULL, run_loop, (void *)i); + } + + /* this thread becomes the first thread */ + run_loop((void *)0); + + /* notreached */ + return 0; +} diff --git a/web/server/h2o/libh2o/src/setuidgid.c b/web/server/h2o/libh2o/src/setuidgid.c new file mode 100644 index 000000000..f663cff18 --- /dev/null +++ b/web/server/h2o/libh2o/src/setuidgid.c @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2015 DeNA Co., Ltd., Kazuho Oku + * + * 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. + */ +#include +#include +#include +#include +#include +#include +#include + +/* taken from sysexits.h */ +#ifndef EX_OSERR +#define EX_OSERR 71 +#endif +#ifndef EX_CONFIG +#define EX_CONFIG 78 +#endif + +int main(int argc, char **argv) +{ + struct passwd *user; + + if (argc < 3) { + fprintf(stderr, "no command (usage: setuidgid user cmd args...)\n"); + return EX_CONFIG; + } + --argc; + ++argv; + + errno = 0; + if ((user = getpwnam(*argv)) == NULL) { + if (errno == 0) { + fprintf(stderr, "unknown user:%s\n", *argv); + return EX_CONFIG; + } else { + perror("getpwnam"); + return EX_OSERR; + } + } + --argc; + ++argv; + + if (setgid(user->pw_gid) != 0) { + perror("setgid failed"); + return EX_OSERR; + } + if (initgroups(user->pw_name, user->pw_gid) != 0) { + perror("initgroups failed"); + return EX_OSERR; + } + if (setuid(user->pw_uid) != 0) { + perror("setuid failed"); + return EX_OSERR; + } + + execvp(*argv, argv); + fprintf(stderr, "execvp failed to launch file:%s:%s\n", *argv, strerror(errno)); + return EX_OSERR; +} diff --git a/web/server/h2o/libh2o/src/ssl.c b/web/server/h2o/libh2o/src/ssl.c new file mode 100644 index 000000000..7ac6c4c96 --- /dev/null +++ b/web/server/h2o/libh2o/src/ssl.c @@ -0,0 +1,962 @@ +/* + * Copyright (c) 2015 DeNA Co., Ltd., Kazuho Oku + * + * 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "yoml-parser.h" +#include "yrmcds.h" +#if H2O_USE_PICOTLS +#include "picotls.h" +#include "picotls/openssl.h" +#endif +#include "h2o/file.h" +#include "h2o.h" +#include "h2o/configurator.h" +#include "standalone.h" + +struct st_session_ticket_generating_updater_conf_t { + const EVP_CIPHER *cipher; + const EVP_MD *md; +}; + +struct st_session_ticket_file_updater_conf_t { + const char *filename; +}; + +static struct { + struct { + void (*setup)(SSL_CTX **contexts, size_t num_contexts); + union { + struct { + size_t num_threads; + char *prefix; + } memcached; + } vars; + } cache; + struct { + void *(*update_thread)(void *conf); + union { + struct st_session_ticket_generating_updater_conf_t generating; + struct { + struct st_session_ticket_generating_updater_conf_t generating; /* at same address as conf.ticket.vars.generating */ + h2o_iovec_t key; + } memcached; + struct st_session_ticket_file_updater_conf_t file; + } vars; + } ticket; + unsigned lifetime; + struct { + char *host; + uint16_t port; + int text_protocol; + } memcached; +} conf; + +H2O_NORETURN static void *cache_cleanup_thread(void *_contexts) +{ + SSL_CTX **contexts = _contexts; + + while (1) { + size_t i; + for (i = 0; contexts[i] != NULL; ++i) + SSL_CTX_flush_sessions(contexts[i], time(NULL)); + sleep(conf.lifetime / 4); + } +} + +static void spawn_cache_cleanup_thread(SSL_CTX **_contexts, size_t num_contexts) +{ + /* copy the list of contexts */ + SSL_CTX **contexts = malloc(sizeof(*contexts) * (num_contexts + 1)); + h2o_memcpy(contexts, _contexts, sizeof(*contexts) * num_contexts); + contexts[num_contexts] = NULL; + + /* launch the thread */ + pthread_t tid; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, 1); + h2o_multithread_create_thread(&tid, &attr, cache_cleanup_thread, contexts); +} + +static void setup_cache_disable(SSL_CTX **contexts, size_t num_contexts) +{ + size_t i; + for (i = 0; i != num_contexts; ++i) + SSL_CTX_set_session_cache_mode(contexts[i], SSL_SESS_CACHE_OFF); +} + +static void setup_cache_internal(SSL_CTX **contexts, size_t num_contexts) +{ + size_t i; + for (i = 0; i != num_contexts; ++i) { + SSL_CTX_set_session_cache_mode(contexts[i], SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_AUTO_CLEAR); + SSL_CTX_set_timeout(contexts[i], conf.lifetime); + } + spawn_cache_cleanup_thread(contexts, num_contexts); +} + +static void setup_cache_memcached(SSL_CTX **contexts, size_t num_contexts) +{ + h2o_memcached_context_t *memc_ctx = + h2o_memcached_create_context(conf.memcached.host, conf.memcached.port, conf.memcached.text_protocol, + conf.cache.vars.memcached.num_threads, conf.cache.vars.memcached.prefix); + h2o_accept_setup_async_ssl_resumption(memc_ctx, conf.lifetime); + size_t i; + for (i = 0; i != num_contexts; ++i) { + SSL_CTX_set_session_cache_mode(contexts[i], SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_AUTO_CLEAR); + SSL_CTX_set_timeout(contexts[i], conf.lifetime); + h2o_socket_ssl_async_resumption_setup_ctx(contexts[i]); + } + spawn_cache_cleanup_thread(contexts, num_contexts); +} + +static void cache_init_defaults(void) +{ + conf.cache.setup = setup_cache_internal; +} + +#if H2O_USE_SESSION_TICKETS + +struct st_session_ticket_t { + unsigned char name[16]; + struct { + const EVP_CIPHER *cipher; + unsigned char *key; + } cipher; + struct { + const EVP_MD *md; + unsigned char *key; + } hmac; + uint64_t not_before; + uint64_t not_after; +}; + +typedef H2O_VECTOR(struct st_session_ticket_t *) session_ticket_vector_t; + +static struct { + pthread_rwlock_t rwlock; + session_ticket_vector_t tickets; /* sorted from newer to older */ +} session_tickets = { +/* we need writer-preferred lock, but on linux PTHREAD_RWLOCK_INITIALIZER is reader-preferred */ +#ifdef PTHREAD_RWLOCK_WRITER_NONRECURSIVE_INITIALIZER_NP + PTHREAD_RWLOCK_WRITER_NONRECURSIVE_INITIALIZER_NP +#else + PTHREAD_RWLOCK_INITIALIZER +#endif + , + {NULL} /* tickets */ +}; + +static struct st_session_ticket_t *new_ticket(const EVP_CIPHER *cipher, const EVP_MD *md, uint64_t not_before, uint64_t not_after, + int fill_in) +{ + int key_len = EVP_CIPHER_key_length(cipher), block_size = EVP_MD_block_size(md); + struct st_session_ticket_t *ticket = h2o_mem_alloc(sizeof(*ticket) + key_len + block_size); + + ticket->cipher.cipher = cipher; + ticket->cipher.key = (unsigned char *)ticket + sizeof(*ticket); + ticket->hmac.md = md; + ticket->hmac.key = ticket->cipher.key + key_len; + ticket->not_before = not_before; + ticket->not_after = not_after; + if (fill_in) { + RAND_bytes(ticket->name, sizeof(ticket->name)); + RAND_bytes(ticket->cipher.key, key_len); + RAND_bytes(ticket->hmac.key, block_size); + } + + return ticket; +} + +static void free_ticket(struct st_session_ticket_t *ticket) +{ + int key_len = EVP_CIPHER_key_length(ticket->cipher.cipher), block_size = EVP_MD_block_size(ticket->hmac.md); + h2o_mem_set_secure(ticket, 0, sizeof(*ticket) + key_len + block_size); + free(ticket); +} + +static int ticket_sort_compare(const void *_x, const void *_y) +{ + struct st_session_ticket_t *x = *(void **)_x, *y = *(void **)_y; + + if (x->not_before != y->not_before) + return x->not_before > y->not_before ? -1 : 1; + return memcmp(x->name, y->name, sizeof(x->name)); +} + +static void free_tickets(session_ticket_vector_t *tickets) +{ + size_t i; + for (i = 0; i != tickets->size; ++i) + free_ticket(tickets->entries[i]); + free(tickets->entries); + memset(tickets, 0, sizeof(*tickets)); +} + +static struct st_session_ticket_t *find_ticket_for_encryption(session_ticket_vector_t *tickets, uint64_t now) +{ + size_t i; + + for (i = 0; i != tickets->size; ++i) { + struct st_session_ticket_t *ticket = tickets->entries[i]; + if (ticket->not_before <= now) { + if (now <= ticket->not_after) { + return ticket; + } else { + return NULL; + } + } + } + return NULL; +} + +static int ticket_key_callback(unsigned char *key_name, unsigned char *iv, EVP_CIPHER_CTX *ctx, HMAC_CTX *hctx, int enc) +{ + int ret; + pthread_rwlock_rdlock(&session_tickets.rwlock); + + if (enc) { + RAND_bytes(iv, EVP_MAX_IV_LENGTH); + struct st_session_ticket_t *ticket = find_ticket_for_encryption(&session_tickets.tickets, time(NULL)), *temp_ticket = NULL; + if (ticket != NULL) { + } else { + /* create a dummy ticket and use (this is the only way to continue the handshake; contrary to the man pages, OpenSSL + * crashes if we return zero */ + ticket = temp_ticket = new_ticket(EVP_aes_256_cbc(), EVP_sha256(), 0, UINT64_MAX, 1); + } + memcpy(key_name, ticket->name, sizeof(ticket->name)); + EVP_EncryptInit_ex(ctx, ticket->cipher.cipher, NULL, ticket->cipher.key, iv); + HMAC_Init_ex(hctx, ticket->hmac.key, EVP_MD_block_size(ticket->hmac.md), ticket->hmac.md, NULL); + if (temp_ticket != NULL) + free_ticket(ticket); + ret = 1; + } else { + struct st_session_ticket_t *ticket; + size_t i; + for (i = 0; i != session_tickets.tickets.size; ++i) { + ticket = session_tickets.tickets.entries[i]; + if (memcmp(ticket->name, key_name, sizeof(ticket->name)) == 0) + goto Found; + } + /* not found */ + ret = 0; + goto Exit; + Found: + EVP_DecryptInit_ex(ctx, ticket->cipher.cipher, NULL, ticket->cipher.key, iv); + HMAC_Init_ex(hctx, ticket->hmac.key, EVP_MD_block_size(ticket->hmac.md), ticket->hmac.md, NULL); + /* Request renewal if the youngest key is active */ + if (i != 0 && session_tickets.tickets.entries[i - 1]->not_before <= time(NULL)) + ret = 2; + else + ret = 1; + } + +Exit: + pthread_rwlock_unlock(&session_tickets.rwlock); + return ret; +} + +static int ticket_key_callback_ossl(SSL *ssl, unsigned char *key_name, unsigned char *iv, EVP_CIPHER_CTX *ctx, HMAC_CTX *hctx, + int enc) +{ + return ticket_key_callback(key_name, iv, ctx, hctx, enc); +} + +#if H2O_USE_PICOTLS + +static int encrypt_ticket_key_ptls(ptls_encrypt_ticket_t *self, ptls_t *tls, int is_encrypt, ptls_buffer_t *dst, ptls_iovec_t src) +{ + return (is_encrypt ? ptls_openssl_encrypt_ticket : ptls_openssl_decrypt_ticket)(dst, src, ticket_key_callback); +} + +#endif + +static int update_tickets(session_ticket_vector_t *tickets, uint64_t now) +{ + int altered = 0, has_valid_ticket; + + /* remove old entries */ + while (tickets->size != 0) { + struct st_session_ticket_t *oldest = tickets->entries[tickets->size - 1]; + if (now <= oldest->not_after) + break; + tickets->entries[--tickets->size] = NULL; + free_ticket(oldest); + altered = 1; + } + + /* create new entry if necessary */ + has_valid_ticket = find_ticket_for_encryption(tickets, now) != NULL; + if (!has_valid_ticket || (tickets->entries[0]->not_before + conf.lifetime / 4 < now)) { + uint64_t not_before = has_valid_ticket ? now + 60 : now; + struct st_session_ticket_t *ticket = new_ticket(conf.ticket.vars.generating.cipher, conf.ticket.vars.generating.md, + not_before, not_before + conf.lifetime - 1, 1); + h2o_vector_reserve(NULL, tickets, tickets->size + 1); + memmove(tickets->entries + 1, tickets->entries, sizeof(tickets->entries[0]) * tickets->size); + ++tickets->size; + tickets->entries[0] = ticket; + altered = 1; + } + + return altered; +} + +H2O_NORETURN static void *ticket_internal_updater(void *unused) +{ + while (1) { + pthread_rwlock_wrlock(&session_tickets.rwlock); + update_tickets(&session_tickets.tickets, time(NULL)); + pthread_rwlock_unlock(&session_tickets.rwlock); + /* sleep for certain amount of time */ + sleep(120 - (h2o_rand() >> 16) % 7); + } +} + +static int serialize_ticket_entry(char *buf, size_t bufsz, struct st_session_ticket_t *ticket) +{ + char *name_buf = alloca(sizeof(ticket->name) * 2 + 1); + h2o_hex_encode(name_buf, ticket->name, sizeof(ticket->name)); + int key_len = EVP_CIPHER_key_length(ticket->cipher.cipher), block_size = EVP_MD_block_size(ticket->hmac.md); + char *key_buf = alloca((key_len + block_size) * 2 + 1); + h2o_hex_encode(key_buf, ticket->cipher.key, key_len); + h2o_hex_encode(key_buf + key_len * 2, ticket->hmac.key, block_size); + + return snprintf(buf, bufsz, "- name: %s\n" + " cipher: %s\n" + " hash: %s\n" + " key: %s\n" + " not_before: %" PRIu64 "\n" + " not_after: %" PRIu64 "\n", + name_buf, OBJ_nid2sn(EVP_CIPHER_type(ticket->cipher.cipher)), OBJ_nid2sn(EVP_MD_type(ticket->hmac.md)), key_buf, + ticket->not_before, ticket->not_after); +} + +static struct st_session_ticket_t *parse_ticket_entry(yoml_t *element, char *errstr) +{ + yoml_t *t; + struct st_session_ticket_t *ticket; + unsigned char name[sizeof(ticket->name) + 1], *key; + const EVP_CIPHER *cipher; + const EVP_MD *hash; + uint64_t not_before, not_after; + + errstr[0] = '\0'; + + if (element->type != YOML_TYPE_MAPPING) { + strcpy(errstr, "node is not a mapping"); + return NULL; + } + +#define FETCH(n, post) \ + do { \ + if ((t = yoml_get(element, n)) == NULL) { \ + strcpy(errstr, " mandatory attribute `" n "` is missing"); \ + return NULL; \ + } \ + if (t->type != YOML_TYPE_SCALAR) { \ + strcpy(errstr, "attribute `" n "` is not a string"); \ + return NULL; \ + } \ + post \ + } while (0) + + FETCH("name", { + if (strlen(t->data.scalar) != sizeof(ticket->name) * 2) { + strcpy(errstr, "length of `name` attribute is not 32 bytes"); + return NULL; + } + if (h2o_hex_decode(name, t->data.scalar, sizeof(ticket->name) * 2) != 0) { + strcpy(errstr, "failed to decode the hex-encoded name"); + return NULL; + } + }); + FETCH("cipher", { + if ((cipher = EVP_get_cipherbyname(t->data.scalar)) == NULL) { + strcpy(errstr, "cannot find the named cipher algorithm"); + return NULL; + } + }); + FETCH("hash", { + if ((hash = EVP_get_digestbyname(t->data.scalar)) == NULL) { + strcpy(errstr, "cannot find the named hash algorgithm"); + return NULL; + } + }); + FETCH("key", { + size_t keylen = EVP_CIPHER_key_length(cipher) + EVP_MD_block_size(hash); + if (strlen(t->data.scalar) != keylen * 2) { + sprintf(errstr, "length of the `key` attribute is incorrect (is %zu, must be %zu)\n", strlen(t->data.scalar), + keylen * 2); + return NULL; + } + key = alloca(keylen + 1); + if (h2o_hex_decode(key, t->data.scalar, keylen * 2) != 0) { + strcpy(errstr, "failed to decode the hex-encoded key"); + return NULL; + } + }); + FETCH("not_before", { + if (sscanf(t->data.scalar, "%" SCNu64, ¬_before) != 1) { + strcpy(errstr, "failed to parse the `not_before` attribute"); + return NULL; + } + }); + FETCH("not_after", { + if (sscanf(t->data.scalar, "%" SCNu64, ¬_after) != 1) { + strcpy(errstr, "failed to parse the `not_after` attribute"); + return NULL; + } + }); + if (!(not_before <= not_after)) { + strcpy(errstr, "`not_after` is not equal to or greater than `not_before`"); + return NULL; + } + +#undef FETCH + + ticket = new_ticket(cipher, hash, not_before, not_after, 0); + memcpy(ticket->name, name, sizeof(ticket->name)); + memcpy(ticket->cipher.key, key, EVP_CIPHER_key_length(cipher)); + memcpy(ticket->hmac.key, key + EVP_CIPHER_key_length(cipher), EVP_MD_block_size(hash)); + return ticket; +} + +static int parse_tickets(session_ticket_vector_t *tickets, const void *src, size_t len, char *errstr) +{ + yaml_parser_t parser; + yoml_t *doc; + size_t i; + + *tickets = (session_ticket_vector_t){NULL}; + yaml_parser_initialize(&parser); + + yaml_parser_set_input_string(&parser, src, len); + yoml_parse_args_t parse_args = {NULL, h2o_mem_set_secure}; + if ((doc = yoml_parse_document(&parser, NULL, &parse_args)) == NULL) { + sprintf(errstr, "parse error at line %d:%s\n", (int)parser.problem_mark.line, parser.problem); + goto Error; + } + if (doc->type != YOML_TYPE_SEQUENCE) { + strcpy(errstr, "root element is not a sequence"); + goto Error; + } + for (i = 0; i != doc->data.sequence.size; ++i) { + char errbuf[256]; + struct st_session_ticket_t *ticket = parse_ticket_entry(doc->data.sequence.elements[i], errbuf); + if (ticket == NULL) { + sprintf(errstr, "at element index %zu:%s\n", i, errbuf); + goto Error; + } + h2o_vector_reserve(NULL, tickets, tickets->size + 1); + tickets->entries[tickets->size++] = ticket; + } + + yoml_free(doc, h2o_mem_set_secure); + yaml_parser_delete(&parser); + return 0; +Error: + if (doc != NULL) + yoml_free(doc, h2o_mem_set_secure); + yaml_parser_delete(&parser); + free_tickets(tickets); + return -1; +} + +static h2o_iovec_t serialize_tickets(session_ticket_vector_t *tickets) +{ + h2o_iovec_t data = {h2o_mem_alloc(tickets->size * 1024 + 1), 0}; + size_t i; + + for (i = 0; i != tickets->size; ++i) { + struct st_session_ticket_t *ticket = tickets->entries[i]; + size_t l = serialize_ticket_entry(data.base + data.len, 1024, ticket); + if (l > 1024) { + fprintf(stderr, "[src/ssl.c] %s:internal buffer overflow\n", __func__); + goto Error; + } + data.len += l; + } + + return data; +Error: + free(data.base); + return (h2o_iovec_t){NULL}; +} + +static int ticket_memcached_update_tickets(yrmcds *conn, h2o_iovec_t key, time_t now) +{ + yrmcds_response resp; + yrmcds_error err; + uint32_t serial; + session_ticket_vector_t tickets = {NULL}; + h2o_iovec_t tickets_serialized = {NULL}; + int retry = 0; + char errbuf[256]; + + /* retrieve tickets on memcached */ + if ((err = yrmcds_get(conn, key.base, key.len, 0, &serial)) != 0) { + fprintf(stderr, "[lib/ssl.c] %s:yrmcds_get failed:%s\n", __func__, yrmcds_strerror(err)); + goto Exit; + } + if ((err = yrmcds_recv(conn, &resp)) != 0) { + fprintf(stderr, "[lib/ssl.c] %s:yrmcds_recv failed:%s\n", __func__, yrmcds_strerror(err)); + goto Exit; + } + if (resp.serial != serial) { + fprintf(stderr, "[lib/ssl.c] %s:unexpected response\n", __func__); + goto Exit; + } + if (resp.status == YRMCDS_STATUS_OK) { + int r = parse_tickets(&tickets, resp.data, resp.data_len, errbuf); + h2o_mem_set_secure((void *)resp.data, 0, resp.data_len); + if (r != 0) { + fprintf(stderr, "[lib/ssl.c] %s:failed to parse response:%s\n", __func__, errbuf); + goto Exit; + } + } + if (tickets.size > 1) + qsort(tickets.entries, tickets.size, sizeof(tickets.entries[0]), ticket_sort_compare); + + /* if we need to update the tickets, atomically update the value in memcached, and request refetch to the caller */ + if (update_tickets(&tickets, now) != 0) { + tickets_serialized = serialize_tickets(&tickets); + if (resp.status == YRMCDS_STATUS_NOTFOUND) { + if ((err = yrmcds_add(conn, key.base, key.len, tickets_serialized.base, tickets_serialized.len, 0, conf.lifetime, 0, 0, + &serial)) != 0) { + fprintf(stderr, "[lib/ssl.c] %s:yrmcds_add failed:%s\n", __func__, yrmcds_strerror(err)); + goto Exit; + } + } else { + if ((err = yrmcds_set(conn, key.base, key.len, tickets_serialized.base, tickets_serialized.len, 0, conf.lifetime, + resp.cas_unique, 0, &serial)) != 0) { + fprintf(stderr, "[lib/ssl.c] %s:yrmcds_set failed:%s\n", __func__, yrmcds_strerror(err)); + goto Exit; + } + } + if ((err = yrmcds_recv(conn, &resp)) != 0) { + fprintf(stderr, "[lib/ssl.c] %s:yrmcds_recv failed:%s\n", __func__, yrmcds_strerror(err)); + goto Exit; + } + retry = 1; + goto Exit; + } + + /* store the results */ + pthread_rwlock_wrlock(&session_tickets.rwlock); + h2o_mem_swap(&session_tickets.tickets, &tickets, sizeof(tickets)); + pthread_rwlock_unlock(&session_tickets.rwlock); + +Exit: + free(tickets_serialized.base); + free_tickets(&tickets); + return retry; +} + +H2O_NORETURN static void *ticket_memcached_updater(void *unused) +{ + while (1) { + /* connect */ + yrmcds conn; + yrmcds_error err; + size_t failcnt; + for (failcnt = 0; (err = yrmcds_connect(&conn, conf.memcached.host, conf.memcached.port)) != YRMCDS_OK; ++failcnt) { + if (failcnt == 0) + fprintf(stderr, "[src/ssl.c] failed to connect to memcached at %s:%" PRIu16 ", %s\n", conf.memcached.host, + conf.memcached.port, yrmcds_strerror(err)); + sleep(10); + } + if (conf.memcached.text_protocol) + yrmcds_text_mode(&conn); + /* connected */ + while (ticket_memcached_update_tickets(&conn, conf.ticket.vars.memcached.key, time(NULL))) + ; + /* disconnect */ + yrmcds_close(&conn); + sleep(60); + } +} + +static int load_tickets_file(const char *fn) +{ +#define ERR_PREFIX "failed to load session ticket secrets from file:%s:" + + h2o_iovec_t data = {NULL}; + session_ticket_vector_t tickets = {NULL}; + char errbuf[256]; + int ret = -1; + + /* load yaml */ + data = h2o_file_read(fn); + if (data.base == NULL) { + char errbuf[256]; + strerror_r(errno, errbuf, sizeof(errbuf)); + fprintf(stderr, ERR_PREFIX "%s\n", fn, errbuf); + goto Exit; + } + /* parse the data */ + if (parse_tickets(&tickets, data.base, data.len, errbuf) != 0) { + fprintf(stderr, ERR_PREFIX "%s\n", fn, errbuf); + goto Exit; + } + /* sort the ticket entries being read */ + if (tickets.size > 1) + qsort(tickets.entries, tickets.size, sizeof(tickets.entries[0]), ticket_sort_compare); + /* replace the ticket list */ + pthread_rwlock_wrlock(&session_tickets.rwlock); + h2o_mem_swap(&session_tickets.tickets, &tickets, sizeof(tickets)); + pthread_rwlock_unlock(&session_tickets.rwlock); + + ret = 0; +Exit: + free(data.base); + free_tickets(&tickets); + return ret; + +#undef ERR_PREFIX +} + +H2O_NORETURN static void *ticket_file_updater(void *unused) +{ + time_t last_mtime = 1; /* file is loaded if mtime changes, 0 is used to indicate that the file was missing */ + + while (1) { + struct stat st; + if (stat(conf.ticket.vars.file.filename, &st) != 0) { + if (last_mtime != 0) { + char errbuf[256]; + strerror_r(errno, errbuf, sizeof(errbuf)); + fprintf(stderr, "cannot load session ticket secrets from file:%s:%s\n", conf.ticket.vars.file.filename, errbuf); + } + last_mtime = 0; + } else if (last_mtime != st.st_mtime) { + /* (re)load */ + last_mtime = st.st_mtime; + if (load_tickets_file(conf.ticket.vars.file.filename) == 0) + fprintf(stderr, "session ticket secrets have been (re)loaded\n"); + } + sleep(10); + } +} + +static void ticket_init_defaults(void) +{ + conf.ticket.update_thread = ticket_internal_updater; + /* to protect the secret >>>2030 we need AES-256 (http://www.keylength.com/en/4/) */ + conf.ticket.vars.generating.cipher = EVP_aes_256_cbc(); + /* integrity checks are only necessary at the time of handshake, and sha256 (recommended by RFC 5077) is sufficient */ + conf.ticket.vars.generating.md = EVP_sha256(); +} + +#endif + +int ssl_session_resumption_on_config(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node) +{ + enum { + MODE_CACHE = 1, + MODE_TICKET = 2, + }; + int modes = -1, uses_memcached; + yoml_t *t; + + if ((t = yoml_get(node, "mode")) == NULL) { + h2o_configurator_errprintf(cmd, node, "mandatory attribute `mode` is missing"); + return -1; + } + if (t->type == YOML_TYPE_SCALAR) { + if (strcasecmp(t->data.scalar, "off") == 0) { + modes = 0; + } else if (strcasecmp(t->data.scalar, "all") == 0) { + modes = MODE_CACHE; +#if H2O_USE_SESSION_TICKETS + modes |= MODE_TICKET; +#endif + } else if (strcasecmp(t->data.scalar, "cache") == 0) { + modes = MODE_CACHE; + } else if (strcasecmp(t->data.scalar, "ticket") == 0) { + modes = MODE_TICKET; + } + } + if (modes == -1) { + h2o_configurator_errprintf(cmd, t, "value of `mode` must be one of: off | all | cache | ticket"); + return -1; + } + + if ((modes & MODE_CACHE) != 0) { + cache_init_defaults(); + if ((t = yoml_get(node, "cache-store")) != NULL) { + if (t->type == YOML_TYPE_SCALAR) { + if (strcasecmp(t->data.scalar, "internal") == 0) { + /* preserve the default */ + t = NULL; + } else if (strcasecmp(t->data.scalar, "memcached") == 0) { + conf.cache.setup = setup_cache_memcached; + t = NULL; + } + } + if (t != NULL) { + h2o_configurator_errprintf(cmd, t, "value of `cache-store` must be one of: internal | memcached"); + return -1; + } + } + if (conf.cache.setup == setup_cache_memcached) { + conf.cache.vars.memcached.num_threads = 1; + conf.cache.vars.memcached.prefix = "h2o:ssl-session-cache:"; + if ((t = yoml_get(node, "cache-memcached-num-threads")) != NULL) { + if (!(t->type == YOML_TYPE_SCALAR && sscanf(t->data.scalar, "%zu", &conf.cache.vars.memcached.num_threads) == 1 && + conf.cache.vars.memcached.num_threads != 0)) { + h2o_configurator_errprintf(cmd, t, "`cache-memcached-num-threads` must be a positive number"); + return -1; + } + } + if ((t = yoml_get(node, "cache-memcached-prefix")) != NULL) { + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, t, "`cache-memcached-prefix` must be a string"); + return -1; + } + conf.cache.vars.memcached.prefix = h2o_strdup(NULL, t->data.scalar, SIZE_MAX).base; + } + } + } else { + conf.cache.setup = setup_cache_disable; + } + + if ((modes & MODE_TICKET) != 0) { +#if H2O_USE_SESSION_TICKETS + ticket_init_defaults(); + if ((t = yoml_get(node, "ticket-store")) != NULL) { + if (t->type == YOML_TYPE_SCALAR) { + if (strcasecmp(t->data.scalar, "internal") == 0) { + /* ok, preserve the defaults */ + t = NULL; + } else if (strcasecmp(t->data.scalar, "file") == 0) { + conf.ticket.update_thread = ticket_file_updater; + t = NULL; + } else if (strcasecmp(t->data.scalar, "memcached") == 0) { + conf.ticket.update_thread = ticket_memcached_updater; + t = NULL; + } + } + if (t != NULL) { + h2o_configurator_errprintf(cmd, t, "value of `ticket-store` must be one of: internal | file"); + return -1; + } + } + if (conf.ticket.update_thread == ticket_internal_updater || conf.ticket.update_thread == ticket_memcached_updater) { + /* generating updater takes two arguments: cipher, hash */ + if ((t = yoml_get(node, "ticket-cipher")) != NULL) { + if (t->type != YOML_TYPE_SCALAR || + (conf.ticket.vars.generating.cipher = EVP_get_cipherbyname(t->data.scalar)) == NULL) { + h2o_configurator_errprintf(cmd, t, "unknown cipher algorithm"); + return -1; + } + } + if ((t = yoml_get(node, "ticket-hash")) != NULL) { + if (t->type != YOML_TYPE_SCALAR || + (conf.ticket.vars.generating.md = EVP_get_digestbyname(t->data.scalar)) == NULL) { + h2o_configurator_errprintf(cmd, t, "unknown hash algorithm"); + return -1; + } + } + if (conf.ticket.update_thread == ticket_memcached_updater) { + conf.ticket.vars.memcached.key = h2o_iovec_init(H2O_STRLIT("h2o:ssl-session-key")); + if ((t = yoml_get(node, "ticket-memcached-prefix")) != NULL) { + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, t, "`ticket-memcached-key` must be a string"); + return -1; + } + conf.ticket.vars.memcached.key = h2o_strdup(NULL, t->data.scalar, SIZE_MAX); + } + } + } else if (conf.ticket.update_thread == ticket_file_updater) { + /* file updater reads the contents of the file and uses it as the session ticket secret */ + if ((t = yoml_get(node, "ticket-file")) == NULL) { + h2o_configurator_errprintf(cmd, node, "mandatory attribute `file` is missing"); + return -1; + } + if (t->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, node, "`file` must be a string"); + return -1; + } + conf.ticket.vars.file.filename = h2o_strdup(NULL, t->data.scalar, SIZE_MAX).base; + } +#else + h2o_configurator_errprintf( + cmd, mode, "ticket-based session resumption cannot be used, the server is built without support for the feature"); + return -1; +#endif + } else { + conf.ticket.update_thread = NULL; + } + + if ((t = yoml_get(node, "memcached")) != NULL) { + conf.memcached.host = NULL; + conf.memcached.port = 11211; + conf.memcached.text_protocol = 0; + size_t index; + for (index = 0; index != t->data.mapping.size; ++index) { + yoml_t *key = t->data.mapping.elements[index].key; + yoml_t *value = t->data.mapping.elements[index].value; + if (value == t) + continue; + if (key->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, key, "attribute must be a string"); + return -1; + } + if (strcmp(key->data.scalar, "host") == 0) { + if (value->type != YOML_TYPE_SCALAR) { + h2o_configurator_errprintf(cmd, value, "`host` must be a string"); + return -1; + } + conf.memcached.host = h2o_strdup(NULL, value->data.scalar, SIZE_MAX).base; + } else if (strcmp(key->data.scalar, "port") == 0) { + if (!(value->type == YOML_TYPE_SCALAR && sscanf(value->data.scalar, "%" SCNu16, &conf.memcached.port) == 1)) { + h2o_configurator_errprintf(cmd, value, "`port` must be a number"); + return -1; + } + } else if (strcmp(key->data.scalar, "protocol") == 0) { + ssize_t sel = h2o_configurator_get_one_of(cmd, value, "BINARY,ASCII"); + if (sel == -1) + return -1; + conf.memcached.text_protocol = (int)sel; + } else { + h2o_configurator_errprintf(cmd, key, "unknown attribute: %s", key->data.scalar); + return -1; + } + } + if (conf.memcached.host == NULL) { + h2o_configurator_errprintf(cmd, t, "mandatory attribute `host` is missing"); + return -1; + } + } + + uses_memcached = conf.cache.setup == setup_cache_memcached; +#if H2O_USE_SESSION_TICKETS + uses_memcached = (uses_memcached || conf.ticket.update_thread == ticket_memcached_updater); +#endif + if (uses_memcached && conf.memcached.host == NULL) { + h2o_configurator_errprintf(cmd, node, "configuration of memcached is missing"); + return -1; + } + + if ((t = yoml_get(node, "lifetime")) != NULL) { + if (!(t->type == YOML_TYPE_SCALAR && sscanf(t->data.scalar, "%u", &conf.lifetime) == 1 && conf.lifetime != 0)) { + h2o_configurator_errprintf(cmd, t, "value of `lifetime` must be a positive number"); + return -1; + } + } + + return 0; +} + +void ssl_setup_session_resumption(SSL_CTX **contexts, size_t num_contexts) +{ + if (conf.cache.setup != NULL) + conf.cache.setup(contexts, num_contexts); + +#if H2O_USE_SESSION_TICKETS + if (num_contexts == 0) + return; + + if (conf.ticket.update_thread != NULL) { + /* start session ticket updater thread */ + pthread_t tid; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, 1); + h2o_multithread_create_thread(&tid, &attr, conf.ticket.update_thread, NULL); + size_t i; + for (i = 0; i != num_contexts; ++i) { + SSL_CTX *ctx = contexts[i]; + SSL_CTX_set_tlsext_ticket_key_cb(ctx, ticket_key_callback_ossl); +#if H2O_USE_PICOTLS + ptls_context_t *pctx = h2o_socket_ssl_get_picotls_context(ctx); + if (pctx != NULL) { + static ptls_encrypt_ticket_t encryptor = {encrypt_ticket_key_ptls}; + pctx->ticket_lifetime = 86400 * 7; // FIXME conf.lifetime; + pctx->encrypt_ticket = &encryptor; + } +#endif + } + } else { + size_t i; + for (i = 0; i != num_contexts; ++i) + SSL_CTX_set_options(contexts[i], SSL_CTX_get_options(contexts[i]) | SSL_OP_NO_TICKET); + } +#endif +} + +static pthread_mutex_t *mutexes; + +static void lock_callback(int mode, int n, const char *file, int line) +{ + if ((mode & CRYPTO_LOCK) != 0) { + pthread_mutex_lock(mutexes + n); + } else if ((mode & CRYPTO_UNLOCK) != 0) { + pthread_mutex_unlock(mutexes + n); + } else { + assert(!"unexpected mode"); + } +} + +static unsigned long thread_id_callback(void) +{ + return (unsigned long)pthread_self(); +} + +static int add_lock_callback(int *num, int amount, int type, const char *file, int line) +{ + (void)type; + (void)file; + (void)line; + + return __sync_add_and_fetch(num, amount); +} + +void init_openssl(void) +{ + int nlocks = CRYPTO_num_locks(), i; + mutexes = h2o_mem_alloc(sizeof(*mutexes) * nlocks); + for (i = 0; i != nlocks; ++i) + pthread_mutex_init(mutexes + i, NULL); + CRYPTO_set_locking_callback(lock_callback); + CRYPTO_set_id_callback(thread_id_callback); + CRYPTO_set_add_lock_callback(add_lock_callback); + + /* Dynamic locks are only used by the CHIL engine at this time */ + + SSL_load_error_strings(); + SSL_library_init(); + OpenSSL_add_all_algorithms(); + + cache_init_defaults(); +#if H2O_USE_SESSION_TICKETS + ticket_init_defaults(); +#endif + conf.lifetime = 3600; /* default value for session timeout is 1 hour */ +} diff --git a/web/server/h2o/libh2o/src/standalone.h b/web/server/h2o/libh2o/src/standalone.h new file mode 100644 index 000000000..567031f4d --- /dev/null +++ b/web/server/h2o/libh2o/src/standalone.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014,2015 DeNA Co., Ltd., Kazuho Oku + * + * 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. + */ +#ifndef h2o__standalone_h +#define h2o__standalone_h + +#include + +#if defined(SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB) && !defined(OPENSSL_NO_TLSEXT) +#define H2O_USE_SESSION_TICKETS 1 +#else +#define H2O_USE_SESSION_TICKETS 0 +#endif + +void init_openssl(void); +void ssl_setup_session_resumption(SSL_CTX **contexts, size_t num_contexts); +int ssl_session_resumption_on_config(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node); + +#endif -- cgit v1.2.3