diff --git a/configure.ac b/configure.ac index 09222f6b..f529aab9 100644 --- a/configure.ac +++ b/configure.ac @@ -237,7 +237,8 @@ AM_CONDITIONAL([ENABLE_SRC], [ test "x${enable_src}" = "xyes" ]) # The example programs depend on OpenSSL enable_examples=no if test "x${request_examples}" = "xyes" && - test "x${have_openssl}" = "xyes"; then + test "x${have_openssl}" = "xyes" && + test "x${have_libevent_openssl}" = "xyes"; then enable_examples=yes fi diff --git a/examples/Makefile.am b/examples/Makefile.am index e10cb9f3..9643fdfc 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -24,14 +24,26 @@ if ENABLE_EXAMPLES AM_CFLAGS = -Wall -AM_CPPFLAGS = -Wall -I$(srcdir)/../lib/includes -I$(builddir)/../lib/includes \ - @OPENSSL_CFLAGS@ @DEFS@ -AM_LDFLAGS = @OPENSSL_LIBS@ +AM_CPPFLAGS = \ + -Wall \ + -I$(srcdir)/../lib/includes \ + -I$(builddir)/../lib/includes \ + -I$(top_srcdir)/src \ + @LIBEVENT_OPENSSL_CFLAGS@ \ + @OPENSSL_CFLAGS@ \ + @DEFS@ +AM_LDFLAGS = \ + @LIBEVENT_OPENSSL_LIBS@ \ + @OPENSSL_LIBS@ LDADD = $(top_builddir)/lib/libnghttp2.la -noinst_PROGRAMS = client +noinst_PROGRAMS = client libevent-client client_SOURCES = client.c +libevent_client_SOURCES = \ + libevent-client.c \ + $(top_srcdir)/src/http-parser/http_parser.c + endif # ENABLE_EXAMPLES EXTRA_DIST = README.rst diff --git a/examples/libevent-client.c b/examples/libevent-client.c new file mode 100644 index 00000000..15afc31a --- /dev/null +++ b/examples/libevent-client.c @@ -0,0 +1,449 @@ +/* + * nghttp2 - HTTP/2.0 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * 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 "http-parser/http_parser.h" + +#define ARRLEN(x) (sizeof(x)/sizeof(x[0])) + +typedef struct { + nghttp2_session *session; + struct evdns_base *dnsbase; + struct bufferevent *bev; + const char *uri; + struct http_parser_url *u; + char *authority; + char *path; + size_t authoritylen; + size_t pathlen; + int32_t stream_id; +} http2_session_data; + +static void drop_connection(http2_session_data *session_data) +{ + bufferevent_free(session_data->bev); + session_data->bev = NULL; + evdns_base_free(session_data->dnsbase, 1); + session_data->dnsbase = NULL; + nghttp2_session_del(session_data->session); + session_data->session = NULL; + free(session_data->path); + session_data->path = NULL; + session_data->pathlen = 0; + free(session_data->authority); + session_data->authority = NULL; + session_data->authoritylen = 0; + session_data->stream_id = -1; +} + +/* Print HTTP headers to |f|. Please note that this function does not + take into account that header name and value are sequence of + octets, therefore they may contain non-printable characters. */ +static void print_headers(FILE *f, nghttp2_nv *nva, size_t nvlen) +{ + size_t i; + for(i = 0; i < nvlen; ++i) { + fwrite(nva[i].name, nva[i].namelen, 1, stderr); + fprintf(stderr, ": "); + fwrite(nva[i].value, nva[i].valuelen, 1, stderr); + fprintf(stderr, "\n"); + } + fprintf(stderr, "\n"); +} + +/* nghttp2_send_callback. Here we transmit the |data|, |length| bytes, + to the network. Because we are using libevent bufferevent, we just + write those bytes into bufferevent buffer. */ +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; + bufferevent_write(bev, data, length); + return length; +} + +/* nghttp2_before_frame_send_callback: Called when nghttp2 library is + about to send a frame. We use this callback to get stream ID of new + stream. Since HEADERS in HTTP/2.0 has several roles, we check that + it is a HTTP request HEADERS. */ +static int before_frame_send_callback +(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) +{ + http2_session_data *session_data = (http2_session_data*)user_data; + if(frame->hd.type == NGHTTP2_HEADERS && + frame->headers.cat == NGHTTP2_HCAT_REQUEST) { + session_data->stream_id = frame->hd.stream_id; + } + return 0; +} + +/* nghttp2_on_frame_recv_callback: Called when nghttp2 library + received a frame from the remote peer. */ +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; + switch(frame->hd.type) { + case NGHTTP2_HEADERS: + if(frame->headers.cat == NGHTTP2_HCAT_RESPONSE && + session_data->stream_id == frame->hd.stream_id) { + /* Print response headers for the initiated request. */ + fprintf(stderr, "Response headers:\n"); + print_headers(stderr, frame->headers.nva, frame->headers.nvlen); + } + break; + } + return 0; +} + +/* nghttp2_on_data_chunk_recv_callback: Called when DATA frame is + received from the remote peer. In this implementation, if the frame + is meant to the stream we initiated, print the received data in + stdout, so that the user can redirect its output to the file + easily. */ +static int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, + const uint8_t *data, size_t len, + void *user_data) +{ + http2_session_data *session_data = (http2_session_data*)user_data; + if(session_data->stream_id == stream_id) { + fwrite(data, len, 1, stdout); + } + return 0; +} + +/* nghttp2_on_stream_close_callback: Called when a stream is about to + closed. This example program only deals with 1 HTTP request (1 + stream), if it is closed, we send GOAWAY and tear down the + session */ +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; + if(session_data->stream_id == stream_id) { + fprintf(stderr, "Stream %d closed with error_code=%d\n", + stream_id, error_code); + nghttp2_submit_goaway(session, NGHTTP2_FLAG_NONE, NGHTTP2_NO_ERROR, + NULL, 0); + } + return 0; +} + +/* NPN TLS extension client callback. We check that server advertised + the HTTP/2.0 protocol the nghttp2 library supports. If not, exit + the program. */ +static int select_next_proto_cb(SSL* ssl, + unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + void *arg) +{ + if(nghttp2_select_next_protocol(out, outlen, in, inlen) <= 0) { + errx(1, "Server did not advertise " NGHTTP2_PROTO_VERSION_ID); + } + return SSL_TLSEXT_ERR_OK; +} + +/* Create SSL_CTX. */ +static SSL_CTX* create_ssl_ctx(void) +{ + SSL_CTX *ssl_ctx; + ssl_ctx = SSL_CTX_new(SSLv23_client_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); + SSL_CTX_set_next_proto_select_cb(ssl_ctx, select_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 initialize_nghttp2_session(http2_session_data *session_data) +{ + nghttp2_session_callbacks callbacks = {0}; + + callbacks.send_callback = send_callback; + callbacks.before_frame_send_callback = before_frame_send_callback; + callbacks.on_frame_recv_callback = on_frame_recv_callback; + callbacks.on_data_chunk_recv_callback = on_data_chunk_recv_callback; + callbacks.on_stream_close_callback = on_stream_close_callback; + nghttp2_session_client_new(&session_data->session, &callbacks, session_data); +} + +static void send_client_connection_header(http2_session_data *session_data) +{ + nghttp2_settings_entry iv[1] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100 } + }; + bufferevent_write(session_data->bev, + NGHTTP2_CLIENT_CONNECTION_HEADER, + NGHTTP2_CLIENT_CONNECTION_HEADER_LEN); + nghttp2_submit_settings(session_data->session, NGHTTP2_FLAG_NONE, + iv, ARRLEN(iv)); +} + +#define MAKE_NV(NAME, VALUE, VALUELEN) \ + { (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, VALUELEN } + +#define MAKE_NV2(NAME, VALUE) \ + { (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1 } + +/* Send HTTP request to the remote peer */ +static void submit_request(http2_session_data *session_data) +{ + const char *uri = session_data->uri; + const struct http_parser_url *u = session_data->u; + nghttp2_nv hdrs[] = { + MAKE_NV2(":method", "GET"), + MAKE_NV(":scheme", + &uri[u->field_data[UF_SCHEMA].off], u->field_data[UF_SCHEMA].len), + MAKE_NV(":authority", session_data->authority, session_data->authoritylen), + MAKE_NV(":path", session_data->path, session_data->pathlen) + }; + fprintf(stderr, "Request headers:\n"); + print_headers(stderr, hdrs, ARRLEN(hdrs)); + nghttp2_submit_request(session_data->session, NGHTTP2_PRI_DEFAULT, + hdrs, ARRLEN(hdrs), NULL, NULL); +} + +/* 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); +} + +/* readcb for bufferevent. Here we get the data from the input buffer + of bufferevent and feed them to nghttp2 library. This may invoke + nghttp2 callbacks. It may also queues the frame in nghttp2 session + context. To send them, we call session_send() in the end. */ +static void readcb(struct bufferevent *bev, void *ptr) +{ + http2_session_data *session_data = (http2_session_data*)ptr; + int rv; + struct evbuffer *input = bufferevent_get_input(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)); + drop_connection(session_data); + } else { + evbuffer_drain(input, rv); + } + session_send(session_data); +} + +/* 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. */ +static void writecb(struct bufferevent *bev, void *ptr) +{ + http2_session_data *session_data = (http2_session_data*)ptr; + if(nghttp2_session_want_read(session_data->session) == 0 && + nghttp2_session_want_write(session_data->session) == 0 && + evbuffer_get_length(bufferevent_get_output(session_data->bev)) == 0) { + drop_connection(session_data); + } +} + +/* eventcb for bufferevent. For the purpose of simplicity and + readability of the example program, we omitted the certificate and + peer verification. After SSL/TLS handshake is over, initialize + nghttp2 library session, and send client connection header. Then + send HTTP request. */ +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, "Connected\n"); + initialize_nghttp2_session(session_data); + send_client_connection_header(session_data); + submit_request(session_data); + session_send(session_data); + return; + } + if(events & BEV_EVENT_EOF) { + warnx("Disconnected from the remote host"); + } else if(events & BEV_EVENT_ERROR) { + warnx("Network error"); + } else if(events & BEV_EVENT_TIMEOUT) { + warnx("Timeout"); + } + drop_connection(session_data); +} + +/* Start connecting to the remote peer |host:port| */ +static void initiate_connection(struct event_base *evbase, + SSL *ssl, const char *host, uint16_t port, + http2_session_data *session_data) +{ + int rv; + struct bufferevent *bev; + bev = bufferevent_openssl_socket_new(evbase, -1, ssl, + BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_DEFER_CALLBACKS | + BEV_OPT_CLOSE_ON_FREE); + bufferevent_setcb(bev, readcb, writecb, eventcb, session_data); + rv = bufferevent_socket_connect_hostname(bev, session_data->dnsbase, + AF_UNSPEC, host, port); + + if(rv != 0) { + errx(1, "Could not connect to the remote host %s", host); + } + session_data->bev = bev; +} + +/* Initializes |session_data| */ +static void initialize_session_data(http2_session_data *session_data, + struct event_base *evbase, + const char *uri, + struct http_parser_url *u) +{ + memset(session_data, 0, sizeof(http2_session_data)); + session_data->dnsbase = evdns_base_new(evbase, 1); + session_data->uri = uri; + session_data->u = u; + session_data->stream_id = -1; + + session_data->authoritylen = u->field_data[UF_HOST].len; + if(u->field_set & (1 << UF_PORT)) { + /* MAX 5 digits (max 65535) + 1 ':' + 1 NULL (because of snprintf) */ + size_t extra = 7; + session_data->authority = malloc(session_data->authoritylen + extra); + session_data->authoritylen += + snprintf(session_data->authority + u->field_data[UF_HOST].len, extra, + ":%u", u->port); + } + memcpy(session_data->authority, + &uri[u->field_data[UF_HOST].off], u->field_data[UF_HOST].len); + + if(u->field_set & (1 << UF_PATH)) { + session_data->pathlen = u->field_data[UF_PATH].len; + } + if(u->field_set & (1 << UF_QUERY)) { + /* +1 for '?' character */ + session_data->pathlen += u->field_data[UF_QUERY].len + 1; + } + session_data->path = malloc(session_data->pathlen); + memcpy(session_data->path, + &uri[u->field_data[UF_PATH].off], u->field_data[UF_PATH].len); + memcpy(session_data->path + u->field_data[UF_PATH].len + 1, + &uri[u->field_data[UF_QUERY].off], u->field_data[UF_QUERY].len); +} + +/* Get resource denoted by the |uri|. The debug and error messages are + printed in stderr, while the response body is printed in stdout. */ +static void run(const char *uri) +{ + struct http_parser_url u; + char *host; + uint16_t port; + int rv; + SSL_CTX *ssl_ctx; + SSL *ssl; + struct event_base *evbase; + http2_session_data session_data; + + /* Parse the |uri| and stores its components in |u| */ + rv = http_parser_parse_url(uri, strlen(uri), 0, &u); + if(rv != 0) { + errx(1, "Could not parse URI %s", uri); + } + host = strndup(&uri[u.field_data[UF_HOST].off], u.field_data[UF_HOST].len); + if(!(u.field_set & (1 << UF_PORT))) { + port = 443; + } else { + port = u.port; + } + + ssl_ctx = create_ssl_ctx(); + ssl = create_ssl(ssl_ctx); + + evbase = event_base_new(); + + initialize_session_data(&session_data, evbase, uri, &u); + + initiate_connection(evbase, ssl, host, port, &session_data); + free(host); + host = NULL; + + 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 < 2) { + fprintf(stderr, "Usage: libevent-client HTTPS_URI\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]); + return 0; +}