/* * 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 #endif /* HAVE_CONFIG_H */ #include #include #ifdef HAVE_UNISTD_H # include #endif /* HAVE_UNISTD_H */ #ifdef HAVE_FCNTL_H # include #endif /* HAVE_FCNTL_H */ #include #ifdef HAVE_SYS_SOCKET_H # include #endif /* HAVE_SYS_SOCKET_H */ #ifdef HAVE_NETDB_H # include #endif /* HAVE_NETDB_H */ #ifdef HAVE_NETINET_IN_H # include #endif /* HAVE_NETINET_IN_H */ #include #include #include #include #include #include #include #include #include #include #include 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); } #ifndef OPENSSL_NO_NEXTPROTONEG /* * Callback function for TLS NPN. Since this program only supports * HTTP/2 protocol, if server does not offer HTTP/2 the nghttp2 * library supports, we terminate program. */ static int select_next_proto_cb(SSL *ssl, unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg) { int rv; (void)ssl; (void)arg; /* nghttp2_select_next_protocol() selects HTTP/2 protocol the nghttp2 library supports. */ rv = nghttp2_select_next_protocol(out, outlen, in, inlen); if (rv <= 0) { die("Server did not advertise HTTP/2 protocol"); } return SSL_TLSEXT_ERR_OK; } #endif /* !OPENSSL_NO_NEXTPROTONEG */ /* * 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); /* Set NPN callback */ #ifndef OPENSSL_NO_NEXTPROTONEG SSL_CTX_set_next_proto_select_cb(ssl_ctx, select_next_proto_cb, NULL); #endif /* !OPENSSL_NO_NEXTPROTONEG */ #if OPENSSL_VERSION_NUMBER >= 0x10002000L SSL_CTX_set_alpn_protos(ssl_ctx, (const unsigned char *)"\x02h2", 3); #endif /* OPENSSL_VERSION_NUMBER >= 0x10002000L */ } 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); #if OPENSSL_VERSION_NUMBER >= 0x1010000fL /* No explicit initialization is required. */ #elif defined(OPENSSL_IS_BORINGSSL) CRYPTO_library_init(); #else /* !(OPENSSL_VERSION_NUMBER >= 0x1010000fL) && \ !defined(OPENSSL_IS_BORINGSSL) */ OPENSSL_config(NULL); SSL_load_error_strings(); SSL_library_init(); OpenSSL_add_all_algorithms(); #endif /* !(OPENSSL_VERSION_NUMBER >= 0x1010000fL) && \ !defined(OPENSSL_IS_BORINGSSL) */ rv = parse_uri(&uri, argv[1]); if (rv != 0) { die("parse_uri failed"); } fetch_uri(&uri); return EXIT_SUCCESS; }