From 4e01ed1a95852541b22c6dd0bbaefbf6539eb6f9 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Mon, 23 Dec 2013 23:27:30 +0900 Subject: [PATCH] examples: Add libevent-server.c --- examples/Makefile.am | 4 +- examples/libevent-server.c | 616 +++++++++++++++++++++++++++++++++++++ 2 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 examples/libevent-server.c diff --git a/examples/Makefile.am b/examples/Makefile.am index 9643fdfc..10d50252 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -37,13 +37,15 @@ AM_LDFLAGS = \ @OPENSSL_LIBS@ LDADD = $(top_builddir)/lib/libnghttp2.la -noinst_PROGRAMS = client libevent-client +noinst_PROGRAMS = client libevent-client libevent-server client_SOURCES = client.c libevent_client_SOURCES = \ libevent-client.c \ $(top_srcdir)/src/http-parser/http_parser.c +libevent_server_SOURCES = libevent-server.c + endif # ENABLE_EXAMPLES EXTRA_DIST = README.rst diff --git a/examples/libevent-server.c b/examples/libevent-server.c new file mode 100644 index 00000000..6883329b --- /dev/null +++ b/examples/libevent-server.c @@ -0,0 +1,616 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include + +#define OUTPUT_WOULDBLOCK_THRESHOLD (1 << 16) + +#define ARRLEN(x) (sizeof(x)/sizeof(x[0])) + +#define MAKE_NV(NAME, VALUE) \ + { (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1 } + +static unsigned char next_proto_list[256]; +static size_t next_proto_list_len; + +struct app_context; +typedef struct app_context app_context; + +typedef struct http2_stream_data { + struct http2_stream_data *prev, *next; + char *request_path; + int32_t stream_id; + int fd; +} http2_stream_data; + +typedef struct http2_session_data { + struct http2_stream_data root; + struct http2_session_data *prev, *next; + struct bufferevent *bev; + app_context *app_ctx; + nghttp2_session *session; + char *client_addr; + size_t handshake_leftlen; +} http2_session_data; + +struct app_context { + SSL_CTX *ssl_ctx; + struct event_base *evbase; +}; + +static int next_proto_cb(SSL *s, const unsigned char **data, unsigned int *len, + void *arg) +{ + *data = next_proto_list; + *len = next_proto_list_len; + return SSL_TLSEXT_ERR_OK; +} + +/* Create SSL_CTX. */ +static SSL_CTX* create_ssl_ctx(const char *key_file, const char *cert_file) +{ + SSL_CTX *ssl_ctx; + ssl_ctx = SSL_CTX_new(SSLv23_server_method()); + if(!ssl_ctx) { + errx(1, "Could not create SSL/TLS context: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + SSL_CTX_set_options(ssl_ctx, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + if(SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, + SSL_FILETYPE_PEM) != 1) { + errx(1, "Could not read private key file %s", key_file); + } + if(SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_file) != 1) { + errx(1, "Could not read certificate file %s", cert_file); + } + + next_proto_list[0] = NGHTTP2_PROTO_VERSION_ID_LEN; + memcpy(&next_proto_list[1], NGHTTP2_PROTO_VERSION_ID, + NGHTTP2_PROTO_VERSION_ID_LEN); + next_proto_list_len = 1 + NGHTTP2_PROTO_VERSION_ID_LEN; + + SSL_CTX_set_next_protos_advertised_cb(ssl_ctx, next_proto_cb, NULL); + return ssl_ctx; +} + +/* Create SSL object */ +static SSL* create_ssl(SSL_CTX *ssl_ctx) +{ + SSL *ssl; + ssl = SSL_new(ssl_ctx); + if(!ssl) { + errx(1, "Could not create SSL/TLS session object: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + return ssl; +} + +static void add_stream(http2_session_data *session_data, + http2_stream_data *stream_data) +{ + stream_data->next = session_data->root.next; + session_data->root.next = stream_data; + stream_data->prev = &session_data->root; + if(stream_data->next) { + stream_data->next->prev = stream_data; + } +} + +static void remove_stream(http2_session_data *session_data, + http2_stream_data *stream_data) +{ + stream_data->prev->next = stream_data->next; + if(stream_data->next) { + stream_data->next->prev = stream_data->prev; + } +} + +static http2_stream_data* create_http2_stream_data +(http2_session_data *session_data, int32_t stream_id) +{ + http2_stream_data *stream_data; + stream_data = malloc(sizeof(http2_stream_data)); + memset(stream_data, 0, sizeof(http2_stream_data)); + stream_data->stream_id = stream_id; + stream_data->fd = -1; + + add_stream(session_data, stream_data); + return stream_data; +} + +static void delete_http2_stream_data(http2_stream_data *stream_data) +{ + if(stream_data->fd != -1) { + close(stream_data->fd); + } + free(stream_data->request_path); + free(stream_data); +} + +static http2_session_data* create_http2_session_data(app_context *app_ctx, + int fd, + struct sockaddr *addr, + int addrlen) +{ + int rv; + http2_session_data *session_data; + SSL *ssl; + char host[NI_MAXHOST]; + + ssl = create_ssl(app_ctx->ssl_ctx); + session_data = malloc(sizeof(http2_session_data)); + memset(session_data, 0, sizeof(http2_session_data)); + session_data->app_ctx = app_ctx; + session_data->bev = bufferevent_openssl_socket_new + (app_ctx->evbase, fd, ssl, + BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + session_data->handshake_leftlen = NGHTTP2_CLIENT_CONNECTION_HEADER_LEN; + rv = getnameinfo(addr, addrlen, host, sizeof(host), NULL, 0, NI_NUMERICHOST); + if(rv != 0) { + session_data->client_addr = strdup("(unknown)"); + } else { + session_data->client_addr = strdup(host); + } + + return session_data; +} + +static void delete_http2_session_data(http2_session_data *session_data) +{ + http2_stream_data *stream_data; + + bufferevent_free(session_data->bev); + nghttp2_session_del(session_data->session); + for(stream_data = session_data->root.next; stream_data;) { + http2_stream_data *next = stream_data->next; + delete_http2_stream_data(stream_data); + stream_data = next; + } + free(session_data->client_addr); + free(session_data); +} + +/* Serialize the frame and send (or buffer) the data to + bufferevent. */ +static void session_send(http2_session_data *session_data) +{ + nghttp2_session_send(session_data->session); +} + +/* Read the data in the bufferevent and feed them into nghttp2 library + function. Invocation of nghttp2_session_mem_recv() may make + additional pending frames, so call session_send() at the end of the + function. */ +static void session_recv(http2_session_data *session_data) +{ + int rv; + struct evbuffer *input = bufferevent_get_input(session_data->bev); + size_t datalen = evbuffer_get_length(input); + unsigned char *data = evbuffer_pullup(input, -1); + rv = nghttp2_session_mem_recv(session_data->session, data, datalen); + if(rv < 0) { + warnx("Fatal error: %s", nghttp2_strerror(rv)); + delete_http2_session_data(session_data); + } else { + evbuffer_drain(input, rv); + } + session_send(session_data); +} + +static ssize_t send_callback(nghttp2_session *session, + const uint8_t *data, size_t length, + int flags, void *user_data) +{ + http2_session_data *session_data = (http2_session_data*)user_data; + struct bufferevent *bev = session_data->bev; + /* Avoid excessive buffering in server side. */ + if(evbuffer_get_length(bufferevent_get_output(session_data->bev)) >= + OUTPUT_WOULDBLOCK_THRESHOLD) { + return NGHTTP2_ERR_WOULDBLOCK; + } + bufferevent_write(bev, data, length); + return length; +} + +/* Returns nonzero if the string |s| ends with the substring |sub| */ +static int ends_with(const char *s, const char *sub) +{ + size_t slen = strlen(s); + size_t sublen = strlen(sub); + if(slen < sublen) { + return 0; + } + return memcmp(s + slen - sublen, sub, sublen) == 0; +} + +/* Returns int value of hex string character |c| */ +static uint8_t hex_to_uint(uint8_t c) +{ + if('0' <= c && c <= '9') { + return c - '0'; + } + if('A' <= c && c <= 'F') { + return c - 'A' + 10; + } + if('a' <= c && c <= 'f') { + return c - 'a' + 10; + } + return 0; +} + +/* Decodes percent-encoded byte string |value| with length |valuelen| + and returns the decoded byte string in allocated buffer. The return + value is NULL terminated. The caller must free the returned + string. */ +static char* percent_decode(const uint8_t *value, size_t valuelen) +{ + char *res; + + res = malloc(valuelen + 1); + if(valuelen > 3) { + size_t i, j; + for(i = 0, j = 0; i < valuelen - 2;) { + if(value[i] != '%' || + !isxdigit(value[i + 1]) || !isxdigit(value[i + 2])) { + res[j++] = value[i++]; + continue; + } + res[j++] = (hex_to_uint(value[i + 1]) << 4) + hex_to_uint(value[i + 2]); + i += 3; + } + memcpy(&res[j], &value[i], 2); + res[j + 2] = '\0'; + } else { + memcpy(res, value, valuelen); + res[valuelen] = '\0'; + } + return res; +} + +static int on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) +{ + http2_session_data *session_data = (http2_session_data*)user_data; + http2_stream_data *stream_data; + size_t i; + const char PATH[] = ":path"; + switch(frame->hd.type) { + case NGHTTP2_HEADERS: + if(frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + break; + } + stream_data = create_http2_stream_data(session_data, frame->hd.stream_id); + nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, + stream_data); + for(i = 0; i < frame->headers.nvlen; ++i) { + nghttp2_nv *nv = &frame->headers.nva[i]; + if(nv->namelen == sizeof(PATH) - 1 && + memcmp(PATH, nv->name, nv->namelen) == 0) { + stream_data->request_path = percent_decode(nv->value, nv->valuelen); + break; + } + } + break; + default: + break; + } + return 0; +} + +static ssize_t file_read_callback +(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, int *eof, + nghttp2_data_source *source, void *user_data) +{ + int fd = source->fd; + ssize_t r; + while((r = read(fd, buf, length)) == -1 && errno == EINTR); + if(r == -1) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + if(r == 0) { + *eof = 1; + } + return r; +} + +static void send_response(nghttp2_session *session, int32_t stream_id, + nghttp2_nv *nva, size_t nvlen, int fd) +{ + nghttp2_data_provider data_prd; + data_prd.source.fd = fd; + data_prd.read_callback = file_read_callback; + + nghttp2_submit_response(session, stream_id, nva, nvlen, &data_prd); +} + +const char ERROR_HTML[] = "404" + "

404 Not Found

"; + +static void error_reply(nghttp2_session *session, + http2_stream_data *stream_data) +{ + int rv; + int pipefd[2]; + nghttp2_nv hdrs[] = { + MAKE_NV(":status", "404") + }; + + rv = pipe(pipefd); + if(rv != 0) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + stream_data->stream_id, + NGHTTP2_INTERNAL_ERROR); + return; + } + write(pipefd[1], ERROR_HTML, sizeof(ERROR_HTML) - 1); + close(pipefd[1]); + stream_data->fd = pipefd[0]; + send_response(session, stream_data->stream_id, hdrs, ARRLEN(hdrs), + pipefd[0]); +} + +/* Minimum check for directory traversal. Returns nonzero if it is + safe. */ +static int check_path(const char *path) +{ + /* We don't like '\' in url. */ + return path[0] && path[0] == '/' && + strchr(path, '\\') == NULL && + strstr(path, "/../") == NULL && + strstr(path, "/./") == NULL && + !ends_with(path, "/..") && !ends_with(path, "/."); +} + +static int on_request_recv_callback(nghttp2_session *session, + int32_t stream_id, void *user_data) +{ + int fd; + http2_session_data *session_data = (http2_session_data*)user_data; + http2_stream_data *stream_data; + nghttp2_nv hdrs[] = { + MAKE_NV(":status", "200") + }; + char *rel_path; + + stream_data = (http2_stream_data*)nghttp2_session_get_stream_user_data + (session, stream_id); + if(!stream_data->request_path) { + error_reply(session, stream_data); + return 0; + } + fprintf(stderr, "%s GET %s\n", session_data->client_addr, + stream_data->request_path); + if(!check_path(stream_data->request_path)) { + error_reply(session, stream_data); + return 0; + } + for(rel_path = stream_data->request_path; *rel_path == '/'; ++rel_path); + fd = open(rel_path, O_RDONLY); + if(fd == -1) { + error_reply(session, stream_data); + return 0; + } + stream_data->fd = fd; + + send_response(session, stream_id, hdrs, ARRLEN(hdrs), fd); + return 0; +} + +static int on_stream_close_callback(nghttp2_session *session, + int32_t stream_id, + nghttp2_error_code error_code, + void *user_data) +{ + http2_session_data *session_data = (http2_session_data*)user_data; + http2_stream_data *stream_data; + + stream_data = nghttp2_session_get_stream_user_data(session, stream_id); + remove_stream(session_data, stream_data); + delete_http2_stream_data(stream_data); + return 0; +} + +static void initialize_nghttp2_session(http2_session_data *session_data) +{ + nghttp2_session_callbacks callbacks = {0}; + + callbacks.send_callback = send_callback; + callbacks.on_frame_recv_callback = on_frame_recv_callback; + callbacks.on_request_recv_callback = on_request_recv_callback; + callbacks.on_stream_close_callback = on_stream_close_callback; + nghttp2_session_server_new(&session_data->session, &callbacks, session_data); +} + +/* Send HTTP/2.0 client connection header, which includes 24 bytes + magic octets and SETTINGS frame */ +static void send_server_connection_header(http2_session_data *session_data) +{ + nghttp2_settings_entry iv[1] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100 } + }; + nghttp2_submit_settings(session_data->session, NGHTTP2_FLAG_NONE, + iv, ARRLEN(iv)); +} + +/* readcb for bufferevent after client connection header was + checked. */ +static void readcb(struct bufferevent *bev, void *ptr) +{ + session_recv((http2_session_data*)ptr); +} + +/* writecb for bufferevent. To greaceful shutdown after sending or + receiving GOAWAY, we check the some conditions on the nghttp2 + library and output buffer of bufferevent. If it indicates we have + no business to this session, tear down the connection. If the + connection is not going to shutdown, we call session_send() to + process pending data in the output buffer. This is necessary + because we have a threshold on the buffer size to avoid too much + buffering. See send_callback(). */ +static void writecb(struct bufferevent *bev, void *ptr) +{ + http2_session_data *session_data = (http2_session_data*)ptr; + if(evbuffer_get_length(bufferevent_get_output(bev)) > 0) { + return; + } + if(nghttp2_session_want_read(session_data->session) == 0 && + nghttp2_session_want_write(session_data->session) == 0) { + delete_http2_session_data(session_data); + return; + } + session_send(session_data); +} + +/* eventcb for bufferevent */ +static void eventcb(struct bufferevent *bev, short events, void *ptr) +{ + http2_session_data *session_data = (http2_session_data*)ptr; + if(events & BEV_EVENT_CONNECTED) { + fprintf(stderr, "%s connected\n", session_data->client_addr); + return; + } + if(events & BEV_EVENT_EOF) { + fprintf(stderr, "%s disconnected\n", session_data->client_addr); + } else if(events & BEV_EVENT_ERROR) { + fprintf(stderr, "%s network error\n", session_data->client_addr); + } else if(events & BEV_EVENT_TIMEOUT) { + fprintf(stderr, "%s timeout\n", session_data->client_addr); + } + delete_http2_session_data(session_data); +} + +/* readcb for bufferevent to check first 24 bytes client connection + header. */ +static void handshake_readcb(struct bufferevent *bev, void *ptr) +{ + http2_session_data *session_data = (http2_session_data*)ptr; + uint8_t data[24]; + struct evbuffer *input = bufferevent_get_input(session_data->bev); + int readlen = evbuffer_remove(input, data, session_data->handshake_leftlen); + const char *conhead = NGHTTP2_CLIENT_CONNECTION_HEADER; + + if(memcmp(conhead + NGHTTP2_CLIENT_CONNECTION_HEADER_LEN + - session_data->handshake_leftlen, data, readlen) != 0) { + delete_http2_session_data(session_data); + return; + } + session_data->handshake_leftlen -= readlen; + if(session_data->handshake_leftlen == 0) { + bufferevent_setcb(session_data->bev, readcb, writecb, eventcb, ptr); + /* Process pending data in buffer since they are not notified + further */ + initialize_nghttp2_session(session_data); + send_server_connection_header(session_data); + session_recv(session_data); + } +} + +/* callback for evconnlistener */ +static void acceptcb(struct evconnlistener *listener, int fd, + struct sockaddr *addr, int addrlen, void *arg) +{ + app_context *app_ctx = (app_context*)arg; + http2_session_data *session_data; + + session_data = create_http2_session_data(app_ctx, fd, addr, addrlen); + bufferevent_setcb(session_data->bev, handshake_readcb, NULL, eventcb, + session_data); +} + +static void start_listen(struct event_base *evbase, const char *service, + app_context *app_ctx) +{ + int rv; + struct addrinfo hints; + struct addrinfo *res, *rp; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; +#ifdef AI_ADDRCONFIG + hints.ai_flags |= AI_ADDRCONFIG; +#endif // AI_ADDRCONFIG + + rv = getaddrinfo(NULL, service, &hints, &res); + if(rv != 0) { + errx(1, NULL); + } + for(rp = res; rp; rp = rp->ai_next) { + struct evconnlistener *listener; + listener = evconnlistener_new_bind(evbase, acceptcb, app_ctx, + LEV_OPT_CLOSE_ON_FREE | + LEV_OPT_REUSEABLE, 16, + rp->ai_addr, rp->ai_addrlen); + if(listener) { + return; + } + } + errx(1, "Could not start listener"); +} + +static void initialize_app_context(app_context *app_ctx, SSL_CTX *ssl_ctx, + struct event_base *evbase) +{ + memset(app_ctx, 0, sizeof(app_context)); + app_ctx->ssl_ctx = ssl_ctx; + app_ctx->evbase = evbase; +} + +static void run(const char *service, + const char *key_file, const char *cert_file) +{ + SSL_CTX *ssl_ctx; + app_context app_ctx; + struct event_base *evbase; + + ssl_ctx = create_ssl_ctx(key_file, cert_file); + evbase = event_base_new(); + initialize_app_context(&app_ctx, ssl_ctx, evbase); + start_listen(evbase, service, &app_ctx); + + event_base_loop(evbase, 0); + + event_base_free(evbase); + SSL_CTX_free(ssl_ctx); +} + +int main(int argc, char **argv) +{ + struct sigaction act; + + if(argc < 4) { + fprintf(stderr, "Usage: libevent-server PORT KEY_FILE CERT_FILE\n"); + exit(EXIT_FAILURE); + } + + memset(&act, 0, sizeof(struct sigaction)); + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, NULL); + + SSL_load_error_strings(); + SSL_library_init(); + + run(argv[1], argv[2], argv[3]); + return 0; +}