diff options
Diffstat (limited to 'examples/client.c')
-rw-r--r-- | examples/client.c | 699 |
1 files changed, 699 insertions, 0 deletions
diff --git a/examples/client.c b/examples/client.c new file mode 100644 index 0000000..ce8d1d0 --- /dev/null +++ b/examples/client.c @@ -0,0 +1,699 @@ +/* + * 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. + */ +/* + * This program is written to show how to use nghttp2 API in C and + * intentionally made simple. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <inttypes.h> +#include <stdlib.h> +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif /* HAVE_UNISTD_H */ +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif /* HAVE_FCNTL_H */ +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif /* HAVE_SYS_SOCKET_H */ +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif /* HAVE_NETDB_H */ +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif /* HAVE_NETINET_IN_H */ +#include <netinet/tcp.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <assert.h> +#include <string.h> +#include <errno.h> + +#include <nghttp2/nghttp2.h> + +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/conf.h> + +enum { IO_NONE, WANT_READ, WANT_WRITE }; + +#define MAKE_NV(NAME, VALUE) \ + { \ + (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \ + NGHTTP2_NV_FLAG_NONE \ + } + +#define MAKE_NV_CS(NAME, VALUE) \ + { \ + (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, strlen(VALUE), \ + NGHTTP2_NV_FLAG_NONE \ + } + +struct Connection { + SSL *ssl; + nghttp2_session *session; + /* WANT_READ if SSL/TLS connection needs more input; or WANT_WRITE + if it needs more output; or IO_NONE. This is necessary because + SSL/TLS re-negotiation is possible at any time. nghttp2 API + offers similar functions like nghttp2_session_want_read() and + nghttp2_session_want_write() but they do not take into account + SSL/TSL connection. */ + int want_io; +}; + +struct Request { + char *host; + /* In this program, path contains query component as well. */ + char *path; + /* This is the concatenation of host and port with ":" in + between. */ + char *hostport; + /* Stream ID for this request. */ + int32_t stream_id; + uint16_t port; +}; + +struct URI { + const char *host; + /* In this program, path contains query component as well. */ + const char *path; + size_t pathlen; + const char *hostport; + size_t hostlen; + size_t hostportlen; + uint16_t port; +}; + +/* + * Returns copy of string |s| with the length |len|. The returned + * string is NULL-terminated. + */ +static char *strcopy(const char *s, size_t len) { + char *dst; + dst = malloc(len + 1); + memcpy(dst, s, len); + dst[len] = '\0'; + return dst; +} + +/* + * Prints error message |msg| and exit. + */ +NGHTTP2_NORETURN +static void die(const char *msg) { + fprintf(stderr, "FATAL: %s\n", msg); + exit(EXIT_FAILURE); +} + +/* + * Prints error containing the function name |func| and message |msg| + * and exit. + */ +NGHTTP2_NORETURN +static void dief(const char *func, const char *msg) { + fprintf(stderr, "FATAL: %s: %s\n", func, msg); + exit(EXIT_FAILURE); +} + +/* + * Prints error containing the function name |func| and error code + * |error_code| and exit. + */ +NGHTTP2_NORETURN +static void diec(const char *func, int error_code) { + fprintf(stderr, "FATAL: %s: error_code=%d, msg=%s\n", func, error_code, + nghttp2_strerror(error_code)); + exit(EXIT_FAILURE); +} + +/* + * The implementation of nghttp2_send_callback type. Here we write + * |data| with size |length| to the network and return the number of + * bytes actually written. See the documentation of + * nghttp2_send_callback for the details. + */ +static ssize_t send_callback(nghttp2_session *session, const uint8_t *data, + size_t length, int flags, void *user_data) { + struct Connection *connection; + int rv; + (void)session; + (void)flags; + + connection = (struct Connection *)user_data; + connection->want_io = IO_NONE; + ERR_clear_error(); + rv = SSL_write(connection->ssl, data, (int)length); + if (rv <= 0) { + int err = SSL_get_error(connection->ssl, rv); + if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) { + connection->want_io = + (err == SSL_ERROR_WANT_READ ? WANT_READ : WANT_WRITE); + rv = NGHTTP2_ERR_WOULDBLOCK; + } else { + rv = NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + return rv; +} + +/* + * The implementation of nghttp2_recv_callback type. Here we read data + * from the network and write them in |buf|. The capacity of |buf| is + * |length| bytes. Returns the number of bytes stored in |buf|. See + * the documentation of nghttp2_recv_callback for the details. + */ +static ssize_t recv_callback(nghttp2_session *session, uint8_t *buf, + size_t length, int flags, void *user_data) { + struct Connection *connection; + int rv; + (void)session; + (void)flags; + + connection = (struct Connection *)user_data; + connection->want_io = IO_NONE; + ERR_clear_error(); + rv = SSL_read(connection->ssl, buf, (int)length); + if (rv < 0) { + int err = SSL_get_error(connection->ssl, rv); + if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) { + connection->want_io = + (err == SSL_ERROR_WANT_READ ? WANT_READ : WANT_WRITE); + rv = NGHTTP2_ERR_WOULDBLOCK; + } else { + rv = NGHTTP2_ERR_CALLBACK_FAILURE; + } + } else if (rv == 0) { + rv = NGHTTP2_ERR_EOF; + } + return rv; +} + +static int on_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + size_t i; + (void)user_data; + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + if (nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)) { + const nghttp2_nv *nva = frame->headers.nva; + printf("[INFO] C ----------------------------> S (HEADERS)\n"); + for (i = 0; i < frame->headers.nvlen; ++i) { + fwrite(nva[i].name, 1, nva[i].namelen, stdout); + printf(": "); + fwrite(nva[i].value, 1, nva[i].valuelen, stdout); + printf("\n"); + } + } + break; + case NGHTTP2_RST_STREAM: + printf("[INFO] C ----------------------------> S (RST_STREAM)\n"); + break; + case NGHTTP2_GOAWAY: + printf("[INFO] C ----------------------------> S (GOAWAY)\n"); + break; + } + return 0; +} + +static int on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + size_t i; + (void)user_data; + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE) { + const nghttp2_nv *nva = frame->headers.nva; + struct Request *req; + req = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id); + if (req) { + printf("[INFO] C <---------------------------- S (HEADERS)\n"); + for (i = 0; i < frame->headers.nvlen; ++i) { + fwrite(nva[i].name, 1, nva[i].namelen, stdout); + printf(": "); + fwrite(nva[i].value, 1, nva[i].valuelen, stdout); + printf("\n"); + } + } + } + break; + case NGHTTP2_RST_STREAM: + printf("[INFO] C <---------------------------- S (RST_STREAM)\n"); + break; + case NGHTTP2_GOAWAY: + printf("[INFO] C <---------------------------- S (GOAWAY)\n"); + break; + } + return 0; +} + +/* + * The implementation of nghttp2_on_stream_close_callback type. We use + * this function to know the response is fully received. Since we just + * fetch 1 resource in this program, after reception of the response, + * we submit GOAWAY and close the session. + */ +static int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) { + struct Request *req; + (void)error_code; + (void)user_data; + + req = nghttp2_session_get_stream_user_data(session, stream_id); + if (req) { + int rv; + rv = nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR); + + if (rv != 0) { + diec("nghttp2_session_terminate_session", rv); + } + } + return 0; +} + +/* + * The implementation of nghttp2_on_data_chunk_recv_callback type. We + * use this function to print the received response body. + */ +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) { + struct Request *req; + (void)flags; + (void)user_data; + + req = nghttp2_session_get_stream_user_data(session, stream_id); + if (req) { + printf("[INFO] C <---------------------------- S (DATA chunk)\n" + "%lu bytes\n", + (unsigned long int)len); + fwrite(data, 1, len, stdout); + printf("\n"); + } + return 0; +} + +/* + * Setup callback functions. nghttp2 API offers many callback + * functions, but most of them are optional. The send_callback is + * always required. Since we use nghttp2_session_recv(), the + * recv_callback is also required. + */ +static void setup_nghttp2_callbacks(nghttp2_session_callbacks *callbacks) { + nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); + + nghttp2_session_callbacks_set_recv_callback(callbacks, recv_callback); + + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, + on_frame_send_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); +} + +/* + * Setup SSL/TLS context. + */ +static void init_ssl_ctx(SSL_CTX *ssl_ctx) { + /* Disable SSLv2 and enable all workarounds for buggy servers */ + SSL_CTX_set_options(ssl_ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + SSL_CTX_set_alpn_protos(ssl_ctx, (const unsigned char *)"\x02h2", 3); +} + +static void ssl_handshake(SSL *ssl, int fd) { + int rv; + if (SSL_set_fd(ssl, fd) == 0) { + dief("SSL_set_fd", ERR_error_string(ERR_get_error(), NULL)); + } + ERR_clear_error(); + rv = SSL_connect(ssl); + if (rv <= 0) { + dief("SSL_connect", ERR_error_string(ERR_get_error(), NULL)); + } +} + +/* + * Connects to the host |host| and port |port|. This function returns + * the file descriptor of the client socket. + */ +static int connect_to(const char *host, uint16_t port) { + struct addrinfo hints; + int fd = -1; + int rv; + char service[NI_MAXSERV]; + struct addrinfo *res, *rp; + snprintf(service, sizeof(service), "%u", port); + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + rv = getaddrinfo(host, service, &hints, &res); + if (rv != 0) { + dief("getaddrinfo", gai_strerror(rv)); + } + for (rp = res; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + continue; + } + while ((rv = connect(fd, rp->ai_addr, rp->ai_addrlen)) == -1 && + errno == EINTR) + ; + if (rv == 0) { + break; + } + close(fd); + fd = -1; + } + freeaddrinfo(res); + return fd; +} + +static void make_non_block(int fd) { + int flags, rv; + while ((flags = fcntl(fd, F_GETFL, 0)) == -1 && errno == EINTR) + ; + if (flags == -1) { + dief("fcntl", strerror(errno)); + } + while ((rv = fcntl(fd, F_SETFL, flags | O_NONBLOCK)) == -1 && errno == EINTR) + ; + if (rv == -1) { + dief("fcntl", strerror(errno)); + } +} + +static void set_tcp_nodelay(int fd) { + int val = 1; + int rv; + rv = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &val, (socklen_t)sizeof(val)); + if (rv == -1) { + dief("setsockopt", strerror(errno)); + } +} + +/* + * Update |pollfd| based on the state of |connection|. + */ +static void ctl_poll(struct pollfd *pollfd, struct Connection *connection) { + pollfd->events = 0; + if (nghttp2_session_want_read(connection->session) || + connection->want_io == WANT_READ) { + pollfd->events |= POLLIN; + } + if (nghttp2_session_want_write(connection->session) || + connection->want_io == WANT_WRITE) { + pollfd->events |= POLLOUT; + } +} + +/* + * Submits the request |req| to the connection |connection|. This + * function does not send packets; just append the request to the + * internal queue in |connection->session|. + */ +static void submit_request(struct Connection *connection, struct Request *req) { + int32_t stream_id; + /* Make sure that the last item is NULL */ + const nghttp2_nv nva[] = {MAKE_NV(":method", "GET"), + MAKE_NV_CS(":path", req->path), + MAKE_NV(":scheme", "https"), + MAKE_NV_CS(":authority", req->hostport), + MAKE_NV("accept", "*/*"), + MAKE_NV("user-agent", "nghttp2/" NGHTTP2_VERSION)}; + + stream_id = nghttp2_submit_request(connection->session, NULL, nva, + sizeof(nva) / sizeof(nva[0]), NULL, req); + + if (stream_id < 0) { + diec("nghttp2_submit_request", stream_id); + } + + req->stream_id = stream_id; + printf("[INFO] Stream ID = %d\n", stream_id); +} + +/* + * Performs the network I/O. + */ +static void exec_io(struct Connection *connection) { + int rv; + rv = nghttp2_session_recv(connection->session); + if (rv != 0) { + diec("nghttp2_session_recv", rv); + } + rv = nghttp2_session_send(connection->session); + if (rv != 0) { + diec("nghttp2_session_send", rv); + } +} + +static void request_init(struct Request *req, const struct URI *uri) { + req->host = strcopy(uri->host, uri->hostlen); + req->port = uri->port; + req->path = strcopy(uri->path, uri->pathlen); + req->hostport = strcopy(uri->hostport, uri->hostportlen); + req->stream_id = -1; +} + +static void request_free(struct Request *req) { + free(req->host); + free(req->path); + free(req->hostport); +} + +/* + * Fetches the resource denoted by |uri|. + */ +static void fetch_uri(const struct URI *uri) { + nghttp2_session_callbacks *callbacks; + int fd; + SSL_CTX *ssl_ctx; + SSL *ssl; + struct Request req; + struct Connection connection; + int rv; + nfds_t npollfds = 1; + struct pollfd pollfds[1]; + + request_init(&req, uri); + + /* Establish connection and setup SSL */ + fd = connect_to(req.host, req.port); + if (fd == -1) { + die("Could not open file descriptor"); + } + ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (ssl_ctx == NULL) { + dief("SSL_CTX_new", ERR_error_string(ERR_get_error(), NULL)); + } + init_ssl_ctx(ssl_ctx); + ssl = SSL_new(ssl_ctx); + if (ssl == NULL) { + dief("SSL_new", ERR_error_string(ERR_get_error(), NULL)); + } + /* To simplify the program, we perform SSL/TLS handshake in blocking + I/O. */ + ssl_handshake(ssl, fd); + + connection.ssl = ssl; + connection.want_io = IO_NONE; + + /* Here make file descriptor non-block */ + make_non_block(fd); + set_tcp_nodelay(fd); + + printf("[INFO] SSL/TLS handshake completed\n"); + + rv = nghttp2_session_callbacks_new(&callbacks); + + if (rv != 0) { + diec("nghttp2_session_callbacks_new", rv); + } + + setup_nghttp2_callbacks(callbacks); + + rv = nghttp2_session_client_new(&connection.session, callbacks, &connection); + + nghttp2_session_callbacks_del(callbacks); + + if (rv != 0) { + diec("nghttp2_session_client_new", rv); + } + + rv = nghttp2_submit_settings(connection.session, NGHTTP2_FLAG_NONE, NULL, 0); + + if (rv != 0) { + diec("nghttp2_submit_settings", rv); + } + + /* Submit the HTTP request to the outbound queue. */ + submit_request(&connection, &req); + + pollfds[0].fd = fd; + ctl_poll(pollfds, &connection); + + /* Event loop */ + while (nghttp2_session_want_read(connection.session) || + nghttp2_session_want_write(connection.session)) { + int nfds = poll(pollfds, npollfds, -1); + if (nfds == -1) { + dief("poll", strerror(errno)); + } + if (pollfds[0].revents & (POLLIN | POLLOUT)) { + exec_io(&connection); + } + if ((pollfds[0].revents & POLLHUP) || (pollfds[0].revents & POLLERR)) { + die("Connection error"); + } + ctl_poll(pollfds, &connection); + } + + /* Resource cleanup */ + nghttp2_session_del(connection.session); + SSL_shutdown(ssl); + SSL_free(ssl); + SSL_CTX_free(ssl_ctx); + shutdown(fd, SHUT_WR); + close(fd); + request_free(&req); +} + +static int parse_uri(struct URI *res, const char *uri) { + /* We only interested in https */ + size_t len, i, offset; + int ipv6addr = 0; + memset(res, 0, sizeof(struct URI)); + len = strlen(uri); + if (len < 9 || memcmp("https://", uri, 8) != 0) { + return -1; + } + offset = 8; + res->host = res->hostport = &uri[offset]; + res->hostlen = 0; + if (uri[offset] == '[') { + /* IPv6 literal address */ + ++offset; + ++res->host; + ipv6addr = 1; + for (i = offset; i < len; ++i) { + if (uri[i] == ']') { + res->hostlen = i - offset; + offset = i + 1; + break; + } + } + } else { + const char delims[] = ":/?#"; + for (i = offset; i < len; ++i) { + if (strchr(delims, uri[i]) != NULL) { + break; + } + } + res->hostlen = i - offset; + offset = i; + } + if (res->hostlen == 0) { + return -1; + } + /* Assuming https */ + res->port = 443; + if (offset < len) { + if (uri[offset] == ':') { + /* port */ + const char delims[] = "/?#"; + int port = 0; + ++offset; + for (i = offset; i < len; ++i) { + if (strchr(delims, uri[i]) != NULL) { + break; + } + if ('0' <= uri[i] && uri[i] <= '9') { + port *= 10; + port += uri[i] - '0'; + if (port > 65535) { + return -1; + } + } else { + return -1; + } + } + if (port == 0) { + return -1; + } + offset = i; + res->port = (uint16_t)port; + } + } + res->hostportlen = (size_t)(uri + offset + ipv6addr - res->host); + for (i = offset; i < len; ++i) { + if (uri[i] == '#') { + break; + } + } + if (i - offset == 0) { + res->path = "/"; + res->pathlen = 1; + } else { + res->path = &uri[offset]; + res->pathlen = i - offset; + } + return 0; +} + +int main(int argc, char **argv) { + struct URI uri; + struct sigaction act; + int rv; + + if (argc < 2) { + die("Specify a https URI"); + } + + memset(&act, 0, sizeof(struct sigaction)); + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, 0); + + rv = parse_uri(&uri, argv[1]); + if (rv != 0) { + die("parse_uri failed"); + } + fetch_uri(&uri); + return EXIT_SUCCESS; +} |