diff options
Diffstat (limited to 'examples/libevent-client.c')
-rw-r--r-- | examples/libevent-client.c | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/examples/libevent-client.c b/examples/libevent-client.c new file mode 100644 index 0000000..0e1c14a --- /dev/null +++ b/examples/libevent-client.c @@ -0,0 +1,595 @@ +/* + * nghttp2 - HTTP/2 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. + */ +#ifdef __sgi +# include <string.h> +# define errx(exitcode, format, args...) \ + { \ + warnx(format, ##args); \ + exit(exitcode); \ + } +# define warnx(format, args...) fprintf(stderr, format "\n", ##args) +char *strndup(const char *s, size_t size); +#endif + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <sys/types.h> +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif /* HAVE_UNISTD_H */ +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif /* HAVE_SYS_SOCKET_H */ +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif /* HAVE_NETINET_IN_H */ +#include <netinet/tcp.h> +#ifndef __sgi +# include <err.h> +#endif +#include <signal.h> +#include <string.h> + +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/conf.h> + +#include <event.h> +#include <event2/event.h> +#include <event2/bufferevent_ssl.h> +#include <event2/dns.h> + +#include <nghttp2/nghttp2.h> + +#include "url-parser/url_parser.h" + +#define ARRLEN(x) (sizeof(x) / sizeof(x[0])) + +typedef struct { + /* The NULL-terminated URI string to retrieve. */ + const char *uri; + /* Parsed result of the |uri| */ + struct http_parser_url *u; + /* The authority portion of the |uri|, not NULL-terminated */ + char *authority; + /* The path portion of the |uri|, including query, not + NULL-terminated */ + char *path; + /* The length of the |authority| */ + size_t authoritylen; + /* The length of the |path| */ + size_t pathlen; + /* The stream ID of this stream */ + int32_t stream_id; +} http2_stream_data; + +typedef struct { + nghttp2_session *session; + struct evdns_base *dnsbase; + struct bufferevent *bev; + http2_stream_data *stream_data; +} http2_session_data; + +static http2_stream_data *create_http2_stream_data(const char *uri, + struct http_parser_url *u) { + /* MAX 5 digits (max 65535) + 1 ':' + 1 NULL (because of snprintf) */ + size_t extra = 7; + http2_stream_data *stream_data = malloc(sizeof(http2_stream_data)); + + stream_data->uri = uri; + stream_data->u = u; + stream_data->stream_id = -1; + + stream_data->authoritylen = u->field_data[UF_HOST].len; + stream_data->authority = malloc(stream_data->authoritylen + extra); + memcpy(stream_data->authority, &uri[u->field_data[UF_HOST].off], + u->field_data[UF_HOST].len); + if (u->field_set & (1 << UF_PORT)) { + stream_data->authoritylen += + (size_t)snprintf(stream_data->authority + u->field_data[UF_HOST].len, + extra, ":%u", u->port); + } + + /* If we don't have path in URI, we use "/" as path. */ + stream_data->pathlen = 1; + if (u->field_set & (1 << UF_PATH)) { + stream_data->pathlen = u->field_data[UF_PATH].len; + } + if (u->field_set & (1 << UF_QUERY)) { + /* +1 for '?' character */ + stream_data->pathlen += (size_t)(u->field_data[UF_QUERY].len + 1); + } + + stream_data->path = malloc(stream_data->pathlen); + if (u->field_set & (1 << UF_PATH)) { + memcpy(stream_data->path, &uri[u->field_data[UF_PATH].off], + u->field_data[UF_PATH].len); + } else { + stream_data->path[0] = '/'; + } + if (u->field_set & (1 << UF_QUERY)) { + stream_data->path[stream_data->pathlen - u->field_data[UF_QUERY].len - 1] = + '?'; + memcpy(stream_data->path + stream_data->pathlen - + u->field_data[UF_QUERY].len, + &uri[u->field_data[UF_QUERY].off], u->field_data[UF_QUERY].len); + } + + return stream_data; +} + +static void delete_http2_stream_data(http2_stream_data *stream_data) { + free(stream_data->path); + free(stream_data->authority); + free(stream_data); +} + +/* Initializes |session_data| */ +static http2_session_data * +create_http2_session_data(struct event_base *evbase) { + http2_session_data *session_data = malloc(sizeof(http2_session_data)); + + memset(session_data, 0, sizeof(http2_session_data)); + session_data->dnsbase = evdns_base_new(evbase, 1); + return session_data; +} + +static void delete_http2_session_data(http2_session_data *session_data) { + SSL *ssl = bufferevent_openssl_get_ssl(session_data->bev); + + if (ssl) { + SSL_shutdown(ssl); + } + 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; + if (session_data->stream_data) { + delete_http2_stream_data(session_data->stream_data); + session_data->stream_data = NULL; + } + free(session_data); +} + +static void print_header(FILE *f, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen) { + fwrite(name, 1, namelen, f); + fprintf(f, ": "); + fwrite(value, 1, valuelen, f); + fprintf(f, "\n"); +} + +/* 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) { + print_header(f, nva[i].name, nva[i].namelen, nva[i].value, nva[i].valuelen); + } + fprintf(f, "\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; + (void)session; + (void)flags; + + bufferevent_write(bev, data, length); + return (ssize_t)length; +} + +/* nghttp2_on_header_callback: Called when nghttp2 library emits + single header name/value pair. */ +static int on_header_callback(nghttp2_session *session, + const nghttp2_frame *frame, const uint8_t *name, + size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags, void *user_data) { + http2_session_data *session_data = (http2_session_data *)user_data; + (void)session; + (void)flags; + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE && + session_data->stream_data->stream_id == frame->hd.stream_id) { + /* Print response headers for the initiated request. */ + print_header(stderr, name, namelen, value, valuelen); + break; + } + } + return 0; +} + +/* nghttp2_on_begin_headers_callback: Called when nghttp2 library gets + started to receive header block. */ +static int on_begin_headers_callback(nghttp2_session *session, + const nghttp2_frame *frame, + void *user_data) { + http2_session_data *session_data = (http2_session_data *)user_data; + (void)session; + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE && + session_data->stream_data->stream_id == frame->hd.stream_id) { + fprintf(stderr, "Response headers for stream ID=%d:\n", + frame->hd.stream_id); + } + break; + } + return 0; +} + +/* nghttp2_on_frame_recv_callback: Called when nghttp2 library + received a complete 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; + (void)session; + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE && + session_data->stream_data->stream_id == frame->hd.stream_id) { + fprintf(stderr, "All headers received\n"); + } + 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; + (void)session; + (void)flags; + + if (session_data->stream_data->stream_id == stream_id) { + fwrite(data, 1, len, 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, + uint32_t error_code, void *user_data) { + http2_session_data *session_data = (http2_session_data *)user_data; + int rv; + + if (session_data->stream_data->stream_id == stream_id) { + fprintf(stderr, "Stream %d closed with error_code=%u\n", stream_id, + error_code); + rv = nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR); + if (rv != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + return 0; +} + +/* Create SSL_CTX. */ +static SSL_CTX *create_ssl_ctx(void) { + SSL_CTX *ssl_ctx; + ssl_ctx = SSL_CTX_new(TLS_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_SSLv3 | + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + SSL_CTX_set_alpn_protos(ssl_ctx, (const unsigned char *)"\x02h2", 3); + + 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; + + nghttp2_session_callbacks_new(&callbacks); + + nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_header_callback(callbacks, + on_header_callback); + + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + + nghttp2_session_client_new(&session_data->session, callbacks, session_data); + + nghttp2_session_callbacks_del(callbacks); +} + +static void send_client_connection_header(http2_session_data *session_data) { + nghttp2_settings_entry iv[1] = { + {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}}; + int rv; + + /* client 24 bytes magic string will be sent by nghttp2 library */ + rv = nghttp2_submit_settings(session_data->session, NGHTTP2_FLAG_NONE, iv, + ARRLEN(iv)); + if (rv != 0) { + errx(1, "Could not submit SETTINGS: %s", nghttp2_strerror(rv)); + } +} + +#define MAKE_NV(NAME, VALUE, VALUELEN) \ + { \ + (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, VALUELEN, \ + NGHTTP2_NV_FLAG_NONE \ + } + +#define MAKE_NV2(NAME, VALUE) \ + { \ + (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \ + NGHTTP2_NV_FLAG_NONE \ + } + +/* Send HTTP request to the remote peer */ +static void submit_request(http2_session_data *session_data) { + int32_t stream_id; + http2_stream_data *stream_data = session_data->stream_data; + const char *uri = stream_data->uri; + const struct http_parser_url *u = stream_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", stream_data->authority, stream_data->authoritylen), + MAKE_NV(":path", stream_data->path, stream_data->pathlen)}; + fprintf(stderr, "Request headers:\n"); + print_headers(stderr, hdrs, ARRLEN(hdrs)); + stream_id = nghttp2_submit_request(session_data->session, NULL, hdrs, + ARRLEN(hdrs), NULL, stream_data); + if (stream_id < 0) { + errx(1, "Could not submit HTTP request: %s", nghttp2_strerror(stream_id)); + } + + stream_data->stream_id = stream_id; +} + +/* Serialize the frame and send (or buffer) the data to + bufferevent. */ +static int session_send(http2_session_data *session_data) { + int rv; + + rv = nghttp2_session_send(session_data->session); + if (rv != 0) { + warnx("Fatal error: %s", nghttp2_strerror(rv)); + return -1; + } + return 0; +} + +/* 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; + ssize_t readlen; + struct evbuffer *input = bufferevent_get_input(bev); + size_t datalen = evbuffer_get_length(input); + unsigned char *data = evbuffer_pullup(input, -1); + + readlen = nghttp2_session_mem_recv(session_data->session, data, datalen); + if (readlen < 0) { + warnx("Fatal error: %s", nghttp2_strerror((int)readlen)); + delete_http2_session_data(session_data); + return; + } + if (evbuffer_drain(input, (size_t)readlen) != 0) { + warnx("Fatal error: evbuffer_drain failed"); + delete_http2_session_data(session_data); + return; + } + if (session_send(session_data) != 0) { + delete_http2_session_data(session_data); + return; + } +} + +/* 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; + (void)bev; + + 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) { + delete_http2_session_data(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) { + int fd = bufferevent_getfd(bev); + int val = 1; + const unsigned char *alpn = NULL; + unsigned int alpnlen = 0; + SSL *ssl; + + fprintf(stderr, "Connected\n"); + + ssl = bufferevent_openssl_get_ssl(session_data->bev); + + if (alpn == NULL) { + SSL_get0_alpn_selected(ssl, &alpn, &alpnlen); + } + + if (alpn == NULL || alpnlen != 2 || memcmp("h2", alpn, 2) != 0) { + fprintf(stderr, "h2 is not negotiated\n"); + delete_http2_session_data(session_data); + return; + } + + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&val, sizeof(val)); + initialize_nghttp2_session(session_data); + send_client_connection_header(session_data); + submit_request(session_data); + if (session_send(session_data) != 0) { + delete_http2_session_data(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"); + } + delete_http2_session_data(session_data); +} + +/* Start connecting to the remote peer |host:port| */ +static void initiate_connection(struct event_base *evbase, SSL_CTX *ssl_ctx, + const char *host, uint16_t port, + http2_session_data *session_data) { + int rv; + struct bufferevent *bev; + SSL *ssl; + + ssl = create_ssl(ssl_ctx); + bev = bufferevent_openssl_socket_new( + evbase, -1, ssl, BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_DEFER_CALLBACKS | BEV_OPT_CLOSE_ON_FREE); + bufferevent_enable(bev, EV_READ | EV_WRITE); + 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; +} + +/* 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; + 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(); + + evbase = event_base_new(); + + session_data = create_http2_session_data(evbase); + session_data->stream_data = create_http2_stream_data(uri, &u); + + initiate_connection(evbase, ssl_ctx, 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); + + run(argv[1]); + return 0; +} |