Mercurial > dovecot > core-2.2
changeset 15394:107c8b2c9594
lib-http: Added initial HTTP client implementation.
author | Stephan Bosch <stephan@rename-it.nl> |
---|---|
date | Sat, 24 Nov 2012 00:30:14 +0200 |
parents | b40bda50541c |
children | fa32c5f9cf71 |
files | src/lib-http/Makefile.am src/lib-http/http-client-connection.c src/lib-http/http-client-host.c src/lib-http/http-client-peer.c src/lib-http/http-client-private.h src/lib-http/http-client-request.c src/lib-http/http-client.c src/lib-http/http-client.h src/lib-http/http-header-parser.c src/lib-http/http-header-parser.h src/lib-http/http-parser.c src/lib-http/http-parser.h src/lib-http/http-response-parser.c src/lib-http/http-response-parser.h src/lib-http/http-transfer-chunked.c src/lib-http/http-transfer.h src/lib-http/test-http-client.c src/lib-http/test-http-header-parser.c src/lib-http/test-http-response-parser.c src/lib-http/test-http-responses.c src/lib-http/test-http-transfer.c |
diffstat | 21 files changed, 4640 insertions(+), 11 deletions(-) [+] |
line wrap: on
line diff
--- a/src/lib-http/Makefile.am Fri Nov 23 23:49:39 2012 +0200 +++ b/src/lib-http/Makefile.am Sat Nov 24 00:30:14 2012 +0200 @@ -8,26 +8,51 @@ libhttp_la_SOURCES = \ http-date.c \ - http-url.c + http-url.c \ + http-parser.c \ + http-header-parser.c \ + http-transfer-chunked.c \ + http-response-parser.c \ + http-client-request.c \ + http-client-connection.c \ + http-client-peer.c \ + http-client-host.c \ + http-client.c headers = \ http-date.h \ - http-url.h + http-url.h \ + http-parser.h \ + http-header-parser.h \ + http-transfer.h \ + http-response-parser.h \ + http-client-private.h \ + http-client.h pkginc_libdir=$(pkgincludedir) pkginc_lib_HEADERS = $(headers) test_programs = \ test-http-date \ - test-http-url + test-http-url \ + test-http-header-parser \ + test-http-transfer \ + test-http-response-parser \ + test-http-client -noinst_PROGRAMS = $(test_programs) +noinst_PROGRAMS = $(test_programs) test-http-responses test_libs = \ ../lib-test/libtest.la \ - ../lib/liblib.la + ../lib/liblib.la \ + ../lib/safe-memset.lo \ + $(MODULE_LIBS) -test_deps = $(noinst_LTLIBRARIES) $(test_libs) +test_deps = \ + $(noinst_LTLIBRARIES) + ../lib-test/libtest.la \ + ../lib/liblib.la \ + ../lib/safe-memset.lo test_http_url_SOURCES = test-http-url.c test_http_url_LDADD = http-url.lo $(test_libs) @@ -37,6 +62,61 @@ test_http_date_LDADD = http-date.lo $(test_libs) test_http_date_DEPENDENCIES = $(test_deps) +test_http_header_parser_SOURCES = test-http-header-parser.c +test_http_header_parser_LDADD = http-parser.lo http-header-parser.lo $(test_libs) +test_http_header_parser_DEPENDENCIES = $(test_deps) + +test_http_transfer_SOURCES = test-http-transfer.c +test_http_transfer_LDADD = \ + http-parser.lo \ + http-header-parser.lo \ + http-transfer-chunked.lo \ + $(test_libs) +test_http_transfer_DEPENDENCIES = $(test_deps) + +test_http_response_parser_SOURCES = test-http-response-parser.c +test_http_response_parser_LDADD = \ + http-date.lo \ + http-parser.lo \ + http-header-parser.lo \ + http-transfer-chunked.lo \ + http-response-parser.lo \ + $(test_libs) +test_http_response_parser_DEPENDENCIES = $(test_deps) + +test_http_client_SOURCES = test-http-client.c +test_http_client_LDFLAGS = -export-dynamic +test_http_client_LDADD = \ + libhttp.la \ + ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + $(test_libs) +test_http_client_DEPENDENCIES = \ + libhttp.la \ + ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + $(test_deps) + +test_http_responses_SOURCES = test-http-responses.c +test_http_responses_LDADD = \ + http-date.lo \ + http-parser.lo \ + http-header-parser.lo \ + http-transfer-chunked.lo \ + http-response-parser.lo \ + ../lib/liblib.la \ + ../lib/safe-memset.lo \ + $(MODULE_LIBS) +test_http_responses_DEPENDENCIES = \ + http-date.lo \ + http-parser.lo \ + http-header-parser.lo \ + http-transfer-chunked.lo \ + http-response-parser.lo \ + $(noinst_LTLIBRARIES) \ + ../lib/liblib.la \ + ../lib/safe-memset.lo + check: check-am check-test check-test: all-am for bin in $(test_programs); do \
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client-connection.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,788 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "hash.h" +#include "array.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "iostream-rawlog.h" +#include "iostream-ssl.h" +#include "http-response-parser.h" + +#include "http-client-private.h" + +/* + * Logging + */ + +static inline void +http_client_connection_debug(struct http_client_connection *conn, + const char *format, ...) ATTR_FORMAT(2, 3); +static inline void +http_client_connection_error(struct http_client_connection *conn, + const char *format, ...) ATTR_FORMAT(2, 3); + +static inline void +http_client_connection_debug(struct http_client_connection *conn, + const char *format, ...) +{ + va_list args; + + if (conn->client->set.debug) { + + va_start(args, format); + i_debug("http-client: conn %s: %s", + http_client_connection_label(conn), t_strdup_vprintf(format, args)); + va_end(args); + } +} + +static inline void +http_client_connection_error(struct http_client_connection *conn, + const char *format, ...) +{ + va_list args; + + va_start(args, format); + i_error("http-client: conn %s: %s", + http_client_connection_label(conn), t_strdup_vprintf(format, args)); + va_end(args); +} + + +/* + * Connection + */ + +static void http_client_connection_input(struct connection *_conn); + +bool http_client_connection_is_ready(struct http_client_connection *conn) +{ + return (conn->connected && !conn->output_locked && + array_count(&conn->request_wait_list) < + conn->client->set.max_pipelined_requests); +} + +static void +http_client_connection_retry_requests(struct http_client_connection *conn, + unsigned int status, const char *error) +{ + struct http_client_request **req; + + array_foreach_modifiable(&conn->request_wait_list, req) { + http_client_request_retry(*req, status, error); + } + array_clear(&conn->request_wait_list); +} + +static void +http_client_connection_server_close(struct http_client_connection **_conn) +{ + struct http_client_connection *conn = *_conn; + struct http_client_request **req; + + conn->connected = FALSE; + conn->closing = TRUE; + + http_client_connection_debug(conn, + "Server explicitly closed connection"); + + array_foreach_modifiable(&conn->request_wait_list, req) { + http_client_request_resubmit(*req); + } + array_clear(&conn->request_wait_list); + + if (conn->client->ioloop != NULL) + io_loop_stop(conn->client->ioloop); + + http_client_connection_free(_conn); +} + +static void +http_client_connection_abort_temp_error(struct http_client_connection **_conn, + unsigned int status, const char *error) +{ + struct http_client_connection *conn = *_conn; + + conn->connected = FALSE; + conn->closing = TRUE; + + http_client_connection_retry_requests(conn, status, error); + http_client_connection_free(_conn); +} + +static void +http_client_connection_abort_error(struct http_client_connection **_conn, + unsigned int status, const char *error) +{ + struct http_client_connection *conn = *_conn; + struct http_client_request **req; + + conn->connected = FALSE; + conn->closing = TRUE; + + array_foreach_modifiable(&conn->request_wait_list, req) { + http_client_request_error(*req, status, error); + } + array_clear(&conn->request_wait_list); + http_client_connection_free(_conn); +} + +static void +http_client_connection_idle_timeout(struct http_client_connection *conn) +{ + http_client_connection_debug(conn, "Idle connection timed out"); + + http_client_connection_free(&conn); +} + +static void +http_client_connection_check_idle(struct http_client_connection *conn) +{ + unsigned int timeout, count; + + if (array_count(&conn->request_wait_list) == 0 && + conn->incoming_payload == NULL && + conn->client->set.max_idle_time_msecs > 0) { + + if (conn->to_idle != NULL) { + /* timeout already set */ + return; + } + + http_client_connection_debug(conn, + "No more requests queued; going idle"); + + if (conn->client->ioloop != NULL) + io_loop_stop(conn->client->ioloop); + + count = array_count(&conn->peer->conns); + i_assert(count > 0); + + /* set timeout for this connection */ + if (count > conn->client->set.max_parallel_connections) { + /* instant death for (urgent) connections above limit */ + timeout = 0; + } else { + /* kill duplicate connections quicker; + linearly based on the number of connections */ + timeout = (conn->client->set.max_parallel_connections - count) * + (conn->client->set.max_idle_time_msecs / + conn->client->set.max_parallel_connections); + } + + conn->to_idle = + timeout_add(timeout, http_client_connection_idle_timeout, conn); + + } else { + /* there should be no idle timeout */ + i_assert(conn->to_idle == NULL); + } +} + +static void +http_client_connection_continue_timeout(struct http_client_connection *conn) +{ + struct http_client_request *const *req_idx; + struct http_client_request *req; + + if (conn->to_response != NULL) + timeout_remove(&conn->to_response); + conn->peer->no_payload_sync = TRUE; + + http_client_connection_debug(conn, + "Expected 100-continue response timed out; sending payload anyway"); + + req_idx = array_idx(&conn->request_wait_list, 0); + req = req_idx[0]; + + // FIXME: we should mark this in the peer object for next requests + conn->payload_continue = TRUE; + if (http_client_request_send_more(req) < 0) { + http_client_connection_abort_temp_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, "Failed to send request"); + } +} + +bool http_client_connection_next_request(struct http_client_connection *conn) +{ + struct http_client_request *req = NULL; + + if (!http_client_connection_is_ready(conn)) { + + http_client_connection_debug(conn, "Not ready for next request"); + return FALSE; + } + + /* claim request, but no urgent request can be second in line */ + req = http_client_peer_claim_request(conn->peer, + array_count(&conn->request_wait_list) > 0); + if (req == NULL) { + http_client_connection_check_idle(conn); + return FALSE; + } + + if (conn->to_idle != NULL) + timeout_remove(&conn->to_idle); + + req->conn = conn; + conn->payload_continue = FALSE; + if (conn->peer->no_payload_sync) + req->payload_sync = FALSE; + array_append(&conn->request_wait_list, &req, 1); + http_client_request_ref(req); + + if (http_client_request_send(req) < 0) { + http_client_connection_abort_temp_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, + "Failed to send request"); + return FALSE; + } + + /* https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-21; + Section 6.1.2.1: + + Because of the presence of older implementations, the protocol allows + ambiguous situations in which a client might send "Expect: 100-continue" + without receiving either a 417 (Expectation Failed) or a 100 (Continue) + status code. Therefore, when a client sends this header field to an + origin server (possibly via a proxy) from which it has never seen a 100 + (Continue) status code, the client SHOULD NOT wait for an indefinite + period before sending the payload body. + */ + if (req->payload_sync) { + i_assert(req->input_size > 0); + i_assert(conn->to_response == NULL); + conn->to_response = timeout_add(HTTP_CLIENT_CONTINUE_TIMEOUT_MSECS, + http_client_connection_continue_timeout, conn); + } + + return TRUE; +} + +static void http_client_connection_destroy(struct connection *_conn) +{ + struct http_client_connection *conn = + (struct http_client_connection *)_conn; + + conn->closing = TRUE; + conn->connected = FALSE; + + switch (_conn->disconnect_reason) { + case CONNECTION_DISCONNECT_CONNECT_TIMEOUT: + http_client_peer_connection_failure(conn->peer); + break; + case CONNECTION_DISCONNECT_CONN_CLOSED: + /* retry pending requests if possible */ + http_client_connection_retry_requests(conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, "Connection lost"); + default: + break; + } + + http_client_connection_free(&conn); +} + +static void http_client_payload_finished(struct http_client_connection *conn) +{ + timeout_remove(&conn->to_input); + conn->conn.io = io_add(conn->conn.fd_in, IO_READ, + http_client_connection_input, &conn->conn); +} + +static void http_client_payload_destroyed(struct http_client_connection *conn) +{ + i_assert(conn->incoming_payload != NULL); + i_assert(conn->pending_request != NULL); + i_assert(conn->conn.io == NULL); + + http_client_connection_debug(conn, "Response payload stream destroyed"); + + /* caller is allowed to change the socket fd to blocking while reading + the payload. make sure here that it's switched back. */ + net_set_nonblock(conn->conn.fd_in, TRUE); + + conn->incoming_payload = NULL; + + http_client_request_finish(&conn->pending_request); + conn->pending_request = NULL; + + if (conn->close_indicated) { + http_client_connection_server_close(&conn); + return; + } + + /* input stream may have pending input. make sure input handler + gets called (but don't do it directly, since we get get here + somewhere from the API user's code, which we can't really know what + state it is in). this call also triggers sending a new request if + necessary. */ + conn->to_input = + timeout_add_short(0, http_client_connection_input, &conn->conn); +} + +static bool +http_client_connection_return_response(struct http_client_connection *conn, + struct http_client_request *req, struct http_response *response) +{ + struct istream *payload; + + i_assert(conn->incoming_payload == NULL); + i_assert(conn->pending_request == NULL); + + req->state = HTTP_REQUEST_STATE_GOT_RESPONSE; + + if (response->payload != NULL) { + /* wrap the stream to capture the destroy event without destroying the + actual payload stream. */ + conn->incoming_payload = response->payload = + i_stream_create_limit(response->payload, (uoff_t)-1); + i_stream_set_destroy_callback(response->payload, + http_client_payload_destroyed, + conn); + /* the callback may add its own I/O, so we need to remove + our one before calling it */ + io_remove(&conn->conn.io); + } + + http_client_request_callback(req, response); + + // FIXME: conn may be freed at this point.. + + if (response->payload != NULL) { + req->state = HTTP_REQUEST_STATE_PAYLOAD_IN; + payload = response->payload; + response->payload = NULL; + conn->pending_request = req; + i_stream_unref(&payload); + if (conn->to_input != NULL) { + /* already finished reading the payload */ + http_client_payload_finished(conn); + } + } else { + http_client_request_finish(&req); + } + + if (conn->incoming_payload == NULL) { + i_assert(conn->conn.io != NULL); + return TRUE; + } + + return FALSE; +} + +static void http_client_connection_input(struct connection *_conn) +{ + struct http_client_connection *conn = + (struct http_client_connection *)_conn; + struct http_response *response; + struct http_client_request *const *req_idx; + struct http_client_request *req = NULL; + int finished = 0, ret; + const char *error; + bool no_payload = FALSE; + + i_assert(conn->incoming_payload == NULL); + + if (conn->to_input != NULL) { + /* We came here from a timeout added by + http_client_payload_destroyed(). The IO couldn't be added + back immediately in there, because the HTTP API user may + still have had its own IO pointed to the same fd. It should + be removed by now, so we can add it back. */ + http_client_payload_finished(conn); + finished++; + } + + /* get first waiting request */ + if (array_count(&conn->request_wait_list) > 0) { + req_idx = array_idx(&conn->request_wait_list, 0); + req = req_idx[0]; + + /* https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-21 + Section 3.3.2: + + A server MAY send a Content-Length header field in a response to a + HEAD request [...] + */ + no_payload = (strcmp(req->method, "HEAD") == 0); + } + + // FIXME: handle somehow if server replies before request->input is at EOF + while ((ret=http_response_parse_next + (conn->http_parser, no_payload, &response, &error)) > 0) { + bool aborted; + + if (req == NULL) { + /* server sent response without any requests in the wait list */ + http_client_connection_error(conn, "Got unexpected input from server"); + http_client_connection_free(&conn); + return; + } + + /* https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-21 + Section 7.2: + + A client MUST be prepared to accept one or more 1xx status responses + prior to a regular response, even if the client does not expect a 100 + (Continue) status message. Unexpected 1xx status responses MAY be + ignored by a user agent. + */ + if (req->payload_sync && response->status == 100) { + conn->payload_continue = TRUE; + if (conn->to_response != NULL) + timeout_remove(&conn->to_response); + http_client_connection_debug(conn, + "Got expected 100-continue response"); + if (http_client_request_send_more(req) < 0) { + http_client_connection_abort_temp_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, + "Failed to send request"); + } + return; + } else if (response->status / 100 == 1) { + /* ignore them for now */ + http_client_connection_debug(conn, + "Got unexpected %u response; ignoring", response->status); + continue; + } // FIXME: handle 417 Expectation Failed + + http_client_connection_debug(conn, + "Got %u response for request %s", + response->status, http_client_request_label(req)); + + /* remove request from queue */ + array_delete(&conn->request_wait_list, 0, 1); + aborted = (req->state == HTTP_REQUEST_STATE_ABORTED); + http_client_request_unref(&req); + + conn->close_indicated = response->connection_close; + + if (!aborted) { + if (response->status / 100 == 3) { + /* redirect */ + http_client_request_redirect(req, response->location); + } else { + /* response for application */ + if (!http_client_connection_return_response(conn, req, response)) + return; + } + + finished++; + } + + /* server closing connection? */ + if (conn->close_indicated) { + http_client_connection_server_close(&conn); + return; + } + + /* get next waiting request */ + if (array_count(&conn->request_wait_list) > 0) { + req_idx = array_idx(&conn->request_wait_list, 0); + req = req_idx[0]; + no_payload = (strcmp(req->method, "HEAD") == 0); + } else { + req = NULL; + no_payload = FALSE; + } + } + + if (ret <= 0 && + (conn->conn.input->eof || conn->conn.input->stream_errno != 0)) { + int stream_errno = conn->conn.input->stream_errno; + http_client_connection_debug(conn, + "Lost connection to server (error=%s)", + stream_errno != 0 ? strerror(stream_errno) : "EOF"); + http_client_connection_abort_temp_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, "Connection lost"); + return; + } + + if (ret < 0) { + http_client_connection_abort_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE, error); + return; + } + + if (finished > 0) { + /* room for new requests */ + http_client_peer_handle_requests(conn->peer); + http_client_connection_check_idle(conn); + } +} + +static int http_client_connection_output(struct http_client_connection *conn) +{ + struct http_client_request *const *req_idx, *req; + struct ostream *output = conn->conn.output; + int ret; + + if ((ret = o_stream_flush(output)) <= 0) { + if (ret < 0) { + http_client_connection_abort_temp_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, "Connection lost"); + } + return ret; + } + + if (array_count(&conn->request_wait_list) > 0 && conn->output_locked) { + req_idx = array_idx(&conn->request_wait_list, 0); + req = req_idx[0]; + + if (!req->payload_sync || conn->payload_continue) { + if (http_client_request_send_more(req) < 0) { + http_client_connection_abort_temp_error(&conn, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, "Connection lost"); + return -1; + } + if (!conn->output_locked) { + /* room for new requests */ + http_client_peer_handle_requests(conn->peer); + http_client_connection_check_idle(conn); + } + } + } + return 1; +} + +static void +http_client_connection_ready(struct http_client_connection *conn) +{ + struct stat st; + + conn->connected = TRUE; + + if (*conn->client->set.rawlog_dir != '\0' && + stat(conn->client->set.rawlog_dir, &st) == 0) { + iostream_rawlog_create(conn->client->set.rawlog_dir, + &conn->conn.input, &conn->conn.output); + } + + conn->http_parser = http_response_parser_init(conn->conn.input); + o_stream_set_flush_callback(conn->conn.output, + http_client_connection_output, conn); + + /* we never pipeline before the first response */ + (void)http_client_connection_next_request(conn); +} + +#ifdef HTTP_BUILD_SSL +static int http_client_connection_ssl_handshaked(void *context) +{ + struct http_client_connection *conn = context; + + if (!conn->client->set.ssl_verify) { + /* skip certificate checks */ + http_client_connection_debug(conn, "SSL handshake successful"); + return 0; + } else if (!ssl_iostream_has_valid_client_cert(conn->ssl_iostream)) { + if (!ssl_iostream_has_broken_client_cert(conn->ssl_iostream)) { + http_client_connection_error(conn, "SSL certificate not received"); + } else { + http_client_connection_error(conn, "Received invalid SSL certificate"); + } + } else { + const char *host = http_client_peer_get_hostname(conn->peer); + + i_assert(host != NULL); + + if (ssl_iostream_cert_match_name(conn->ssl_iostream, host) < 0) { + http_client_connection_error(conn, + "SSL certificate doesn't match host name"); + } else { + http_client_connection_debug(conn, "SSL handshake successful"); + return 0; + } + } + i_stream_close(conn->conn.input); + return -1; +} + +static int +http_client_connection_ssl_init(struct http_client_connection *conn) +{ + struct ssl_iostream_settings ssl_set; + const char *source; + + if (conn->peer->ssl_ctx == NULL) { + http_client_connection_error(conn, "No SSL context"); + return -1; + } + + memset(&ssl_set, 0, sizeof(ssl_set)); + if (conn->client->set.ssl_verify) { + ssl_set.verbose_invalid_cert = TRUE; + ssl_set.verify_remote_cert = TRUE; + ssl_set.require_valid_cert = TRUE; + } + + if (conn->client->set.debug) + http_client_connection_debug(conn, "Starting SSL handshake"); + + source = t_strdup_printf + ("connection %s: ", http_client_connection_label(conn)); + if (io_stream_create_ssl(conn->peer->ssl_ctx, source, &ssl_set, + &conn->conn.input, &conn->conn.output, &conn->ssl_iostream) < 0) { + http_client_connection_error(conn, "Couldn't initialize SSL client"); + return -1; + } + ssl_iostream_set_handshake_callback(conn->ssl_iostream, + http_client_connection_ssl_handshaked, conn); + if (ssl_iostream_handshake(conn->ssl_iostream) < 0) { + http_client_connection_error(conn, "SSL handshake failed: %s", + ssl_iostream_get_last_error(conn->ssl_iostream)); + return -1; + } + + http_client_connection_ready(conn); + return 0; +} +#endif + +static void +http_client_connection_connected(struct connection *_conn, bool success) +{ + struct http_client_connection *conn = + (struct http_client_connection *)_conn; + + if (!success) { + http_client_connection_error(conn, "Connect failed: %m"); + http_client_peer_connection_failure(conn->peer); + + } else { + http_client_connection_debug(conn, "Connected"); +#ifdef HTTP_BUILD_SSL + if (conn->peer->addr.ssl) { + if (http_client_connection_ssl_init(conn) < 0) + http_client_peer_connection_failure(conn->peer); + return; + } +#endif + http_client_connection_ready(conn); + } +} + +static const struct connection_settings http_client_connection_set = { + .input_max_size = (size_t)-1, + .output_max_size = (size_t)-1, + .client = TRUE +}; + +static const struct connection_vfuncs http_client_connection_vfuncs = { + .destroy = http_client_connection_destroy, + .input = http_client_connection_input, + .client_connected = http_client_connection_connected +}; + +struct connection_list * +http_client_connection_list_init(void) +{ + return connection_list_init + (&http_client_connection_set, &http_client_connection_vfuncs); +} + +static int http_client_connection_connect(struct http_client_connection *conn) +{ + if (conn->conn.fd_in == -1) { + if (connection_client_connect(&conn->conn) < 0) { + http_client_connection_error(conn, "Could not connect"); + return -1; + } + } + + return 0; +} + +struct http_client_connection * +http_client_connection_create(struct http_client_peer *peer) +{ + struct http_client_connection *conn; + static unsigned int id = 0; + + conn = i_new(struct http_client_connection, 1); + conn->client = peer->client; + conn->peer = peer; + i_array_init(&conn->request_wait_list, 16); + + connection_init_client_ip + (peer->client->conn_list, &conn->conn, &peer->addr.ip, peer->addr.port); + + if (http_client_connection_connect(conn) < 0) { + http_client_connection_free(&conn); + return NULL; + } + + conn->id = id++; + array_append(&peer->conns, &conn, 1); + + http_client_connection_debug(conn, + "Connection created (%d parallel connections exist)", + array_count(&peer->conns)); + return conn; +} + +void http_client_connection_free(struct http_client_connection **_conn) +{ + struct http_client_connection *conn = *_conn; + struct http_client_connection *const *conn_idx; + struct http_client_peer *peer = conn->peer; + struct http_client_request **req; + + http_client_connection_debug(conn, "Connection destroy"); + + conn->connected = FALSE; + +#ifdef HTTP_BUILD_SSL + if (conn->ssl_iostream != NULL) + ssl_iostream_unref(&conn->ssl_iostream); +#endif + + connection_deinit(&conn->conn); + + /* abort all pending requests */ + array_foreach_modifiable(&conn->request_wait_list, req) { + http_client_request_error(*req, HTTP_CLIENT_REQUEST_ERROR_ABORTED, + "Aborting"); + http_client_request_unref(req); + } + array_free(&conn->request_wait_list); + + if (conn->to_input != NULL) + timeout_remove(&conn->to_input); + if (conn->to_idle != NULL) + timeout_remove(&conn->to_idle); + if (conn->to_response != NULL) + timeout_remove(&conn->to_response); + + /* remove this connection from the list */ + array_foreach(&conn->peer->conns, conn_idx) { + if (*conn_idx == conn) { + array_delete(&conn->peer->conns, + array_foreach_idx(&conn->peer->conns, conn_idx), 1); + break; + } + } + + if (conn->http_parser != NULL) + http_response_parser_deinit(&conn->http_parser); + + i_free(conn); + *_conn = NULL; + + http_client_peer_connection_lost(peer); +} + +void http_client_connection_switch_ioloop(struct http_client_connection *conn) +{ + if (conn->to_input != NULL) + conn->to_input = io_loop_move_timeout(&conn->to_input); + if (conn->to_idle != NULL) + conn->to_idle = io_loop_move_timeout(&conn->to_idle); + if (conn->to_response != NULL) + conn->to_response = io_loop_move_timeout(&conn->to_response); + connection_switch_ioloop(&conn->conn); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client-host.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,388 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "hash.h" +#include "array.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "dns-lookup.h" +#include "http-response-parser.h" + +#include "http-client-private.h" + +/* + * Logging + */ + +static inline void +http_client_host_debug(struct http_client_host *host, + const char *format, ...) ATTR_FORMAT(2, 3); + +static inline void +http_client_host_debug(struct http_client_host *host, + const char *format, ...) +{ + va_list args; + + if (host->client->set.debug) { + + va_start(args, format); + i_debug("http-client: host %s: %s", + host->name, t_strdup_vprintf(format, args)); + va_end(args); + } +} + +/* + * Host:port + */ + +static struct http_client_host_port * +http_client_host_port_find(struct http_client_host *host, + unsigned int port, bool ssl) +{ + struct http_client_host_port *hport; + + array_foreach_modifiable(&host->ports, hport) { + if (hport->port == port && hport->ssl == ssl) + return hport; + } + + return NULL; +} + +static struct http_client_host_port * +http_client_host_port_init(struct http_client_host *host, + unsigned int port, bool ssl) +{ + struct http_client_host_port *hport; + + hport = http_client_host_port_find(host, port, ssl); + if (hport == NULL) { + hport = array_append_space(&host->ports); + hport->port = port; + hport->ssl = ssl; + hport->ips_connect_idx = 0; + i_array_init(&hport->request_queue, 16); + i_array_init(&hport->urgent_request_queue, 4); + } + + return hport; +} + +static void http_client_host_port_error(struct http_client_host_port *hport, + unsigned int status, const char *error) +{ + struct http_client_request **req; + + /* abort all pending requests */ + array_foreach_modifiable(&hport->request_queue, req) { + http_client_request_error(*req, status, error); + } + array_foreach_modifiable(&hport->urgent_request_queue, req) { + http_client_request_error(*req, status, error); + } + array_clear(&hport->request_queue); + array_clear(&hport->urgent_request_queue); +} + +static void http_client_host_port_deinit(struct http_client_host_port *hport) +{ + http_client_host_port_error + (hport, HTTP_CLIENT_REQUEST_ERROR_ABORTED, "Aborted"); + array_free(&hport->request_queue); + array_free(&hport->urgent_request_queue); +} + +static void +http_client_host_port_drop_request(struct http_client_host_port *hport, + struct http_client_request *req) +{ + struct http_client_request **req_idx; + ARRAY_TYPE(http_client_request) *queue = + (req->urgent ? &hport->urgent_request_queue : &hport->request_queue); + unsigned int idx; + + array_foreach_modifiable(queue, req_idx) { + if (*req_idx == req) { + idx = array_foreach_idx(queue, req_idx); + break; + } + } + + array_delete(queue, idx, 1); +} + +/* + * Host + */ + +static void +http_client_host_connection_setup(struct http_client_host *host, + struct http_client_host_port *hport) +{ + struct http_client_peer *peer = NULL; + struct http_client_peer_addr addr; + + while (hport->ips_connect_idx < host->ips_count) { + addr.ip = host->ips[hport->ips_connect_idx]; + addr.port = hport->port; + addr.ssl = hport->ssl; + + http_client_host_debug(host, "Setting up connection to %s:%u (ssl=%s)", + net_ip2addr(&addr.ip), addr.port, (addr.ssl ? "yes" : "no")); + + if ((peer=http_client_peer_get(host->client, &addr)) != NULL) + break; + + hport->ips_connect_idx++; + } + + if (peer == NULL) { + http_client_host_port_error + (hport, HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED, "Connection failed"); + if (host->client->ioloop != NULL) + io_loop_stop(host->client->ioloop); + return; + } + + http_client_peer_add_host(peer, host); +} + +void http_client_host_connection_failure(struct http_client_host *host, + const struct http_client_peer_addr *addr) +{ + struct http_client_host_port *hport; + + http_client_host_debug(host, "Failed to connect to %s:%u", + net_ip2addr(&addr->ip), addr->port); + + hport = http_client_host_port_find(host, addr->port, addr->ssl); + if (hport == NULL) + return; + + hport->ips_connect_idx++; + http_client_host_connection_setup(host, hport); +} + +static void +http_client_host_lookup_failure(struct http_client_host *host) +{ + struct http_client_host_port *hport; + + array_foreach_modifiable(&host->ports, hport) { + http_client_host_port_error(hport, + HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED, "Failed to lookup host"); + } +} + +static void +http_client_host_dns_callback(const struct dns_lookup_result *result, + struct http_client_host *host) +{ + struct http_client_host_port *hport; + unsigned int requests = 0; + + host->dns_lookup = NULL; + + if (result->ret != 0) { + i_error("http-client: dns_lookup(%s) failed: %s", + host->name, result->error); + http_client_host_lookup_failure(host); + return; + } + + http_client_host_debug(host, + "DNS lookup successful; got %d IPs", result->ips_count); + + i_assert(result->ips_count > 0); + host->ips_count = result->ips_count; + host->ips = i_new(struct ip_addr, host->ips_count); + memcpy(host->ips, result->ips, sizeof(*host->ips) * host->ips_count); + + // FIXME: make DNS result expire + + /* make connections to requested ports */ + array_foreach_modifiable(&host->ports, hport) { + unsigned int count = array_count(&hport->request_queue); + hport->ips_connect_idx = 0; + if (count > 0) + http_client_host_connection_setup(host, hport); + requests += count; + } + + if (requests == 0 && host->client->ioloop != NULL) + io_loop_stop(host->client->ioloop); +} + +static void http_client_host_lookup +(struct http_client_host *host) +{ + struct http_client *client = host->client; + struct dns_lookup_settings dns_set; + struct ip_addr ip, *ips; + unsigned int ips_count; + int ret; + + memset(&dns_set, 0, sizeof(dns_set)); + dns_set.dns_client_socket_path = + client->set.dns_client_socket_path; + dns_set.timeout_msecs = HTTP_CLIENT_DNS_LOOKUP_TIMEOUT_MSECS; + + if (host->ips_count == 0 && + net_addr2ip(host->name, &ip) == 0) { // FIXME: remove this? + host->ips_count = 1; + host->ips = i_new(struct ip_addr, host->ips_count); + host->ips[0] = ip; + } else if (*dns_set.dns_client_socket_path == '\0') { + ret = net_gethostbyname(host->name, &ips, &ips_count); + if (ret != 0) { + i_error("http-client: net_gethostbyname(%s) failed: %s", + host->name, net_gethosterror(ret)); + http_client_host_lookup_failure(host); + return; + } + + http_client_host_debug(host, + "DNS lookup successful; got %d IPs", ips_count); + + host->ips_count = ips_count; + host->ips = i_new(struct ip_addr, ips_count); + memcpy(host->ips, ips, ips_count * sizeof(*ips)); + } + + if (host->ips_count == 0) { + http_client_host_debug(host, + "Performing asynchronous DNS lookup"); + (void)dns_lookup(host->name, &dns_set, + http_client_host_dns_callback, host, &host->dns_lookup); + } +} + +struct http_client_host *http_client_host_get +(struct http_client *client, const char *hostname) +{ + struct http_client_host *host; + + host = hash_table_lookup(client->hosts, hostname); + if (host == NULL) { + // FIXME: limit the maximum number of inactive cached hosts + host = i_new(struct http_client_host, 1); + host->client = client; + host->name = i_strdup(hostname); + i_array_init(&host->ports, 4); + + hash_table_insert(client->hosts, hostname, host); + + http_client_host_debug(host, "Host created"); + } + return host; +} + +void http_client_host_submit_request(struct http_client_host *host, + struct http_client_request *req) +{ + struct http_client_host_port *hport; + + req->host = host; + + if (host->ips_count == 0 && host->dns_lookup == NULL) + http_client_host_lookup(host); + hport = http_client_host_port_init(host, req->port, req->ssl); + if (req->urgent) + array_append(&hport->urgent_request_queue, &req, 1); + else + array_append(&hport->request_queue, &req, 1); + + if (host->ips_count == 0) + return; + + http_client_host_connection_setup(host, hport); +} + +struct http_client_request * +http_client_host_claim_request(struct http_client_host *host, + const struct http_client_peer_addr *addr, bool no_urgent) +{ + struct http_client_host_port *hport; + struct http_client_request *const *req_idx; + struct http_client_request *req; + + hport = http_client_host_port_find(host, addr->port, addr->ssl); + if (hport == NULL) + return NULL; + if (!no_urgent && array_count(&hport->urgent_request_queue) > 0) { + req_idx = array_idx(&hport->urgent_request_queue, 0); + req = *req_idx; + array_delete(&hport->urgent_request_queue, 0, 1); + } else if (array_count(&hport->request_queue) > 0) { + req_idx = array_idx(&hport->request_queue, 0); + req = *req_idx; + array_delete(&hport->request_queue, 0, 1); + } else { + return NULL; + } + + http_client_host_debug(host, + "Connection %s:%u claimed request %s %s", + net_ip2addr(&addr->ip), addr->port, http_client_request_label(req), + (req->urgent ? "(urgent)" : "")); + + return req; +} + +bool http_client_host_have_requests(struct http_client_host *host, + const struct http_client_peer_addr *addr, bool urgent) +{ + struct http_client_host_port *hport; + + hport = http_client_host_port_find(host, addr->port, addr->ssl); + if (hport == NULL) + return FALSE; + + if (urgent) + return (array_count(&hport->urgent_request_queue) > 0); + return (array_count(&hport->request_queue) > 0); +} + +void http_client_host_drop_request(struct http_client_host *host, + struct http_client_request *req) +{ + struct http_client_host_port *hport; + + hport = http_client_host_port_find(host, req->port, req->ssl); + if (hport == NULL) + return; + + http_client_host_port_drop_request(hport, req); +} + +void http_client_host_free(struct http_client_host **_host) +{ + struct http_client_host *host = *_host; + struct http_client_host_port *hport; + + http_client_host_debug(host, "Host destroy"); + + if (host->dns_lookup != NULL) + dns_lookup_abort(&host->dns_lookup); + + /* drop request queues */ + array_foreach_modifiable(&host->ports, hport) { + http_client_host_port_deinit(hport); + } + array_free(&host->ports); + + i_free(host->ips); + i_free(host->name); + i_free(host); +} + +void http_client_host_switch_ioloop(struct http_client_host *host) +{ + if (host->dns_lookup != NULL) + dns_lookup_switch_ioloop(host->dns_lookup); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client-peer.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,358 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "hash.h" +#include "array.h" +#include "istream.h" +#include "ostream.h" +#include "iostream-ssl.h" +#include "http-response-parser.h" + +#include "http-client-private.h" + +/* + * Logging + */ + +static inline void +http_client_peer_debug(struct http_client_peer *peer, + const char *format, ...) ATTR_FORMAT(2, 3); + +static inline void +http_client_peer_debug(struct http_client_peer *peer, + const char *format, ...) +{ + va_list args; + + if (peer->client->set.debug) { + va_start(args, format); + i_debug("http-client: peer %s:%u: %s", + net_ip2addr(&peer->addr.ip), peer->addr.port, + t_strdup_vprintf(format, args)); + va_end(args); + } +} + +static inline void ATTR_FORMAT(2, 3) +http_client_peer_error(struct http_client_peer *peer, + const char *format, ...) +{ + va_list args; + va_start(args, format); + i_error("http-client: peer %s:%u: %s", + net_ip2addr(&peer->addr.ip), peer->addr.port, + t_strdup_vprintf(format, args)); + va_end(args); +} + +/* + * Peer address + */ + +unsigned int http_client_peer_addr_hash +(const struct http_client_peer_addr *peer) +{ + return net_ip_hash(&peer->ip) + peer->port + peer->ssl; +} + +int http_client_peer_addr_cmp +(const struct http_client_peer_addr *peer1, + const struct http_client_peer_addr *peer2) +{ + int ret; + + if ((ret=net_ip_cmp(&peer1->ip, &peer2->ip)) != 0) + return ret; + if (peer1->port != peer2->port) + return (peer1->port > peer2->port ? 1 : -1); + if (peer1->ssl != peer2->ssl) + return (peer1->ssl > peer2->ssl ? 1 : -1); + return 0; +} + +/* + * Peer + */ + +const char * +http_client_peer_get_hostname(struct http_client_peer *peer) +{ + struct http_client_host *const *host; + + if (array_count(&peer->hosts) == 0) + return NULL; + + /* just return name of initial host */ + host = array_idx(&peer->hosts, 1); + return (*host)->name; +} + +static int +http_client_peer_connect(struct http_client_peer *peer) +{ + struct http_client_connection *conn; + + conn = http_client_connection_create(peer); + if (conn == NULL) { + http_client_peer_debug(peer, "Failed to make new connection"); + return -1; + } + + return 0; +} + +static bool +http_client_peer_requests_pending(struct http_client_peer *peer, bool urgent) +{ + struct http_client_host *const *host; + + array_foreach(&peer->hosts, host) { + if (http_client_host_have_requests(*host, &peer->addr, urgent)) + return TRUE; + } + + return FALSE; +} + +static bool +http_client_peer_next_request(struct http_client_peer *peer, + bool urgent) +{ + struct http_client_connection *const *conn_idx; + struct http_client_connection *conn = NULL; + unsigned int closing = 0, min_waiting = (unsigned int)-1; + + /* at this point we already know that a request for this peer is pending + */ + + /* find the least busy connection */ + array_foreach(&peer->conns, conn_idx) { + if (http_client_connection_is_ready(*conn_idx)) { + unsigned int waiting = array_count(&(*conn_idx)->request_wait_list); + if (waiting < min_waiting) { + min_waiting = waiting; + conn = *conn_idx; + if (min_waiting == 0) + break; + } + } + /* count the number of closing connections */ + if ((*conn_idx)->closing) + closing++; + } + + /* do we have an idle connection? */ + if (conn != NULL && min_waiting == 0) { + /* yes */ + return http_client_connection_next_request(conn); + } + + /* no, but can we create a new connection? */ + if (!urgent && (array_count(&peer->conns) - closing) >= + peer->client->set.max_parallel_connections) { + /* no */ + if (conn == NULL) + return FALSE; + /* pipeline it */ + return http_client_connection_next_request(conn); + } + + /* yes */ + if (http_client_peer_connect(peer) < 0) { + /* connection failed */ + if (conn == NULL) + return FALSE; + /* pipeline it on the least busy connection we found */ + return http_client_connection_next_request(conn); + } + + /* now we wait until it is connected */ + return FALSE; +} + +void http_client_peer_handle_requests(struct http_client_peer *peer) +{ + /* check urgent requests first */ + while (http_client_peer_requests_pending(peer, TRUE)) { + if (!http_client_peer_next_request(peer, TRUE)) + break; + } + + /* check normal requests once we're done */ + while (http_client_peer_requests_pending(peer, FALSE)) { + if (!http_client_peer_next_request(peer, FALSE)) + break; + } +} + +static struct http_client_peer * +http_client_peer_create(struct http_client *client, + const struct http_client_peer_addr *addr) +{ + struct http_client_peer *peer; +#ifdef HTTP_BUILD_SSL + struct ssl_iostream_settings ssl_set; +#endif + + peer = i_new(struct http_client_peer, 1); + peer->client = client; + peer->addr = *addr; + i_array_init(&peer->hosts, 16); + i_array_init(&peer->conns, 16); + + hash_table_insert + (client->peers, (const struct http_client_peer_addr *)&peer->addr, peer); + + http_client_peer_debug(peer, "Peer created"); + +#ifdef HTTP_BUILD_SSL + if (peer->addr.ssl && peer->ssl_ctx == NULL) { + const char *source; + memset(&ssl_set, 0, sizeof(ssl_set)); + ssl_set.ca_dir = peer->client->set.ssl_ca_dir; + ssl_set.verify_remote_cert = TRUE; + ssl_set.crypto_device = peer->client->set.ssl_crypto_device; + + source = t_strdup_printf("http-client: peer %s:%u", + net_ip2addr(&peer->addr.ip), peer->addr.port); + if (ssl_iostream_context_init_client + (source, &ssl_set, &peer->ssl_ctx) < 0) { + http_client_peer_error(peer, "Couldn't initialize SSL context"); + http_client_peer_free(&peer); + return NULL; + } + } +#else + if (peer->addr.ssl) { + http_client_peer_error(peer, "HTTPS is not supported"); + http_client_peer_free(&peer); + return NULL; + } +#endif + + if (http_client_peer_connect(peer) < 0) { + http_client_peer_free(&peer); + return NULL; + } + + return peer; +} + +void http_client_peer_free(struct http_client_peer **_peer) +{ + struct http_client_peer *peer = *_peer; + struct http_client_connection **conn; + ARRAY_TYPE(http_client_connection) conns; + + if (peer->destroyed) + return; + peer->destroyed = TRUE; + + http_client_peer_debug(peer, "Peer destroy"); + + /* make a copy of the connection array; freed connections modify it */ + t_array_init(&conns, array_count(&peer->conns)); + array_copy(&conns.arr, 0, &peer->conns.arr, 0, array_count(&peer->conns)); + + array_foreach_modifiable(&conns, conn) { + http_client_connection_free(conn); + } + + i_assert(array_count(&peer->conns) == 0); + array_free(&peer->conns); + array_free(&peer->hosts); + +#ifdef HTTP_BUILD_SSL + if (peer->ssl_ctx != NULL) + ssl_iostream_context_deinit(&peer->ssl_ctx); +#endif + + hash_table_remove + (peer->client->peers, (const struct http_client_peer_addr *)&peer->addr); + + i_free(peer); + *_peer = NULL; +} + +struct http_client_peer * +http_client_peer_get(struct http_client *client, + const struct http_client_peer_addr *addr) +{ + struct http_client_peer *peer; + + peer = hash_table_lookup(client->peers, addr); + if (peer == NULL) + peer = http_client_peer_create(client, addr); + + return peer; +} + +void http_client_peer_add_host(struct http_client_peer *peer, + struct http_client_host *host) +{ + struct http_client_host *const *host_idx; + bool exists = FALSE; + + array_foreach(&peer->hosts, host_idx) { + if (*host_idx == host) { + exists = TRUE; + break; + } + } + + if (!exists) + array_append(&peer->hosts, &host, 1); + if (exists || array_count(&peer->hosts) > 1) + (void)http_client_peer_next_request(peer, FALSE); +} + +struct http_client_request * +http_client_peer_claim_request(struct http_client_peer *peer, bool no_urgent) +{ + struct http_client_host *const *host_idx; + struct http_client_request *req; + + array_foreach(&peer->hosts, host_idx) { + if ((req=http_client_host_claim_request + (*host_idx, &peer->addr, no_urgent)) != NULL) { + req->peer = peer; + return req; + } + } + + return NULL; +} + +void http_client_peer_connection_failure(struct http_client_peer *peer) +{ + struct http_client_host *const *host; + + http_client_peer_debug(peer, "Failed to make connection"); + + if (array_count(&peer->conns) == 1) { + array_foreach(&peer->hosts, host) { + http_client_host_connection_failure(*host, &peer->addr); + } + } +} + +void http_client_peer_connection_lost(struct http_client_peer *peer) +{ + if (peer->destroyed) + return; + + http_client_peer_debug(peer, "Lost connection (%d connections left)", + array_count(&peer->conns)); + + if (array_count(&peer->conns) == 0) { + if (http_client_peer_requests_pending(peer, TRUE)) + (void)http_client_peer_next_request(peer, TRUE); + else if (http_client_peer_requests_pending(peer, FALSE)) + (void)http_client_peer_next_request(peer, FALSE); + else + http_client_peer_free(&peer); + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client-private.h Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,250 @@ +#ifndef HTTP_CLIENT_PRIVATE_H +#define HTTP_CLIENT_PRIVATE_H + +#include "connection.h" + +#include "http-client.h" + +#define HTTP_BUILD_SSL + +#define HTTP_DEFAULT_PORT 80 +#define HTTPS_DEFAULT_PORT 443 + +#define HTTP_CLIENT_DNS_LOOKUP_TIMEOUT_MSECS (1000*30) +#define HTTP_CLIENT_CONNECT_TIMEOUT_MSECS (1000*30) +#define HTTP_CLIENT_DEFAULT_REQUEST_TIMEOUT_MSECS (1000*60*5) +#define HTTP_CLIENT_CONTINUE_TIMEOUT_MSECS (1000*2) + +struct http_client_host; +struct http_client_host_port; +struct http_client_peer; +struct http_client_connection; + +ARRAY_DEFINE_TYPE(http_client_host, struct http_client_host *); +ARRAY_DEFINE_TYPE(http_client_host_port, struct http_client_host_port); +ARRAY_DEFINE_TYPE(http_client_connection, struct http_client_connection *); +ARRAY_DEFINE_TYPE(http_client_request, struct http_client_request *); + +HASH_TABLE_DEFINE_TYPE(http_client_host, const char *, + struct http_client_host *); +HASH_TABLE_DEFINE_TYPE(http_client_peer, const struct http_client_peer_addr *, + struct http_client_peer *); + +enum http_request_state { + HTTP_REQUEST_STATE_NEW = 0, + HTTP_REQUEST_STATE_QUEUED, + HTTP_REQUEST_STATE_PAYLOAD_OUT, + HTTP_REQUEST_STATE_WAITING, + HTTP_REQUEST_STATE_GOT_RESPONSE, + HTTP_REQUEST_STATE_PAYLOAD_IN, + HTTP_REQUEST_STATE_FINISHED, + HTTP_REQUEST_STATE_ABORTED +}; + +struct http_client_request { + pool_t pool; + unsigned int refcount; + + struct http_client_request *prev, *next; + + const char *method, *hostname, *target; + unsigned int port; + + struct http_client *client; + struct http_client_host *host; + struct http_client_peer *peer; + struct http_client_connection *conn; + + string_t *headers; + struct istream *input; + uoff_t input_size; + + unsigned int attempts; + unsigned int redirects; + + http_client_request_callback_t *callback; + void *context; + + enum http_request_state state; + + unsigned int payload_sync:1; + unsigned int ssl:1; + unsigned int urgent:1; +}; + +struct http_client_host_port { + unsigned int port; + + /* current index in host->ips */ + unsigned int ips_connect_idx; + + /* requests pending in queue to be picked up by connections */ + ARRAY_TYPE(http_client_request) request_queue; + + /* urgent request queue */ + ARRAY_TYPE(http_client_request) urgent_request_queue; + + unsigned int ssl:1; +}; + +struct http_client_host { + struct http_client *client; + char *name; + + /* the ip addresses DNS returned for this host */ + unsigned int ips_count; + struct ip_addr *ips; + + /* requests are managed on a per-port basis */ + ARRAY_TYPE(http_client_host_port) ports; + + /* active DNS lookup */ + struct dns_lookup *dns_lookup; +}; + +struct http_client_peer_addr { + struct ip_addr ip; + unsigned int port; + unsigned int ssl:1; /* https */ +}; + +struct http_client_peer { + struct http_client_peer_addr addr; + struct http_client *client; + + /* hosts served through this peer */ + ARRAY_TYPE(http_client_host) hosts; + + /* active connections to this peer */ + ARRAY_TYPE(http_client_connection) conns; + + /* ssl */ +#ifdef HTTP_BUILD_SSL + struct ssl_iostream_context *ssl_ctx; +#endif + + unsigned int destroyed:1; /* peer is being destroyed */ + unsigned int no_payload_sync:1; /* expect: 100-continue failed before */ +}; + +struct http_client_connection { + struct connection conn; + struct http_client_peer *peer; + struct http_client *client; + + const char *label; + + unsigned int id; // DEBUG: identify parallel connections + + /* ssl */ +#ifdef HTTP_BUILD_SSL + struct ssl_iostream *ssl_iostream; +#endif + + struct http_response_parser *http_parser; + struct timeout *to_input, *to_idle, *to_response; + + struct http_client_request *pending_request; + struct istream *incoming_payload; + + /* requests that have been sent, waiting for response */ + ARRAY_TYPE(http_client_request) request_wait_list; + + unsigned int connected:1; /* connection is connected */ + unsigned int closing:1; + unsigned int close_indicated:1; + unsigned int output_locked:1; /* output is locked; no pipelining */ + unsigned int payload_continue:1; /* received 100-continue for current + request */ +}; + +struct http_client { + pool_t pool; + + struct http_client_settings set; + + struct ioloop *ioloop; + + struct connection_list *conn_list; + + HASH_TABLE_TYPE(http_client_host) hosts; + HASH_TABLE_TYPE(http_client_peer) peers; + unsigned int pending_requests; +}; + +static inline const char * +http_client_request_label(struct http_client_request *req) +{ + return t_strdup_printf("[%s http%s://%s:%d%s]", + req->method, req->ssl ? "s" : "", req->hostname, req->port, req->target); +} + +static inline const char * +http_client_connection_label(struct http_client_connection *conn) +{ + return t_strdup_printf("%s:%u [%d]", + net_ip2addr(&conn->conn.ip), conn->conn.port, conn->id); +} + +void http_client_request_ref(struct http_client_request *req); +void http_client_request_unref(struct http_client_request **_req); +int http_client_request_send(struct http_client_request *req); +int http_client_request_send_more(struct http_client_request *req); +void http_client_request_callback(struct http_client_request *req, + struct http_response *response); +void http_client_request_resubmit(struct http_client_request *req); +void http_client_request_retry(struct http_client_request *req, + unsigned int status, const char *error); +void http_client_request_error(struct http_client_request *req, + unsigned int status, const char *error); +void http_client_request_redirect(struct http_client_request *req, + const char *location); +void http_client_request_finish(struct http_client_request **_req); + +struct connection_list *http_client_connection_list_init(void); + +struct http_client_connection * + http_client_connection_create(struct http_client_peer *peer); +void http_client_connection_free(struct http_client_connection **_conn); +bool http_client_connection_is_ready(struct http_client_connection *conn); +bool http_client_connection_next_request(struct http_client_connection *conn); +void http_client_connection_switch_ioloop(struct http_client_connection *conn); + +unsigned int http_client_peer_addr_hash + (const struct http_client_peer_addr *peer) ATTR_PURE; +int http_client_peer_addr_cmp + (const struct http_client_peer_addr *peer1, + const struct http_client_peer_addr *peer2) ATTR_PURE; + +const char * + http_client_peer_get_hostname(struct http_client_peer *peer); +struct http_client_peer * + http_client_peer_get(struct http_client *client, + const struct http_client_peer_addr *addr); +void http_client_peer_free(struct http_client_peer **_peer); +void http_client_peer_add_host(struct http_client_peer *peer, + struct http_client_host *host); +struct http_client_request * + http_client_peer_claim_request(struct http_client_peer *peer, + bool no_urgent); +void http_client_peer_handle_requests(struct http_client_peer *peer); +void http_client_peer_connection_failure(struct http_client_peer *peer); +void http_client_peer_connection_lost(struct http_client_peer *peer); + +struct http_client_host * + http_client_host_get(struct http_client *client, const char *hostname); +void http_client_host_free(struct http_client_host **_host); +void http_client_host_submit_request(struct http_client_host *host, + struct http_client_request *req); +struct http_client_request * +http_client_host_claim_request(struct http_client_host *host, + const struct http_client_peer_addr *addr, bool no_urgent); +void http_client_host_connection_failure(struct http_client_host *host, + const struct http_client_peer_addr *addr); +bool http_client_host_have_requests(struct http_client_host *host, + const struct http_client_peer_addr *addr, bool urgent); +void http_client_host_drop_request(struct http_client_host *host, + struct http_client_request *req); +void http_client_host_switch_ioloop(struct http_client_host *host); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client-request.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,386 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "hash.h" +#include "array.h" +#include "istream.h" +#include "ostream.h" +#include "http-url.h" +#include "http-response-parser.h" + +#include "http-client-private.h" + +/* + * Logging + */ + +static inline void +http_client_request_debug(struct http_client_request *req, + const char *format, ...) ATTR_FORMAT(2, 3); + +static inline void +http_client_request_debug(struct http_client_request *req, + const char *format, ...) +{ + va_list args; + + if (req->client->set.debug) { + va_start(args, format); + i_debug("http-client: request %s: %s", + http_client_request_label(req), t_strdup_vprintf(format, args)); + va_end(args); + } +} + +/* + * Request + */ + +#undef http_client_request +struct http_client_request * +http_client_request(struct http_client *client, + const char *method, const char *host, const char *target, + http_client_request_callback_t *callback, void *context) +{ + pool_t pool; + struct http_client_request *req; + + pool = pool_alloconly_create("http client request", 2048); + req = p_new(pool, struct http_client_request, 1); + req->pool = pool; + req->refcount = 1; + req->client = client; + req->method = p_strdup(pool, method); + req->hostname = p_strdup(pool, host); + req->port = HTTP_DEFAULT_PORT; + req->target = p_strdup(pool, target); + req->callback = callback; + req->context = context; + req->headers = str_new(default_pool, 256); + + req->state = HTTP_REQUEST_STATE_NEW; + return req; +} + +void http_client_request_ref(struct http_client_request *req) +{ + req->refcount++; +} + +void http_client_request_unref(struct http_client_request **_req) +{ + struct http_client_request *req = *_req; + struct http_client *client = req->client; + + i_assert(req->refcount > 0); + + if (--req->refcount > 0) + return; + + /* only decrease pending request counter if this request was submitted */ + if (req->state > HTTP_REQUEST_STATE_NEW) + req->client->pending_requests--; + + http_client_request_debug(req, "Destroy (requests left=%d)", + client->pending_requests); + + if (req->input != NULL) + i_stream_unref(&req->input); + str_free(&req->headers); + pool_unref(&req->pool); + *_req = NULL; +} + +void http_client_request_set_port(struct http_client_request *req, + unsigned int port) +{ + i_assert(req->state == HTTP_REQUEST_STATE_NEW); + req->port = port; +} + +void http_client_request_set_ssl(struct http_client_request *req, + bool ssl) +{ + i_assert(req->state == HTTP_REQUEST_STATE_NEW); + if (ssl) { + if (!req->ssl && req->port == HTTP_DEFAULT_PORT) + req->port = HTTPS_DEFAULT_PORT; + } else { + if (req->ssl && req->port == HTTPS_DEFAULT_PORT) + req->port = HTTP_DEFAULT_PORT; + } + req->ssl = ssl; +} + +void http_client_request_set_urgent(struct http_client_request *req) +{ + i_assert(req->state == HTTP_REQUEST_STATE_NEW); + req->urgent = TRUE; +} + +void http_client_request_add_header(struct http_client_request *req, + const char *key, const char *value) +{ + i_assert(req->state == HTTP_REQUEST_STATE_NEW); + str_printfa(req->headers, "%s: %s\r\n", key, value); +} + +void http_client_request_set_payload(struct http_client_request *req, + struct istream *input, bool sync) +{ + i_assert(req->state == HTTP_REQUEST_STATE_NEW); + i_assert(req->input == NULL); + + i_stream_ref(input); + req->input = input; + if (i_stream_get_size(input, TRUE, &req->input_size) <= 0) + i_unreached(); //FIXME + + /* prepare request payload sync using 100 Continue response from server */ + if (req->input_size > 0 && sync) { + req->payload_sync = TRUE; + } +} + +void http_client_request_submit(struct http_client_request *req) +{ + struct http_client_host *host; + + i_assert(req->state == HTTP_REQUEST_STATE_NEW); + http_client_request_debug(req, "Submitted"); + + host = http_client_host_get(req->client, req->hostname); + http_client_host_submit_request(host, req); + req->state = HTTP_REQUEST_STATE_QUEUED; + req->client->pending_requests++; +} + +int http_client_request_send_more(struct http_client_request *req) +{ + struct http_client_connection *conn = req->conn; + struct ostream *output = conn->conn.output; + int ret = 0; + + i_assert(req->input != NULL); + + o_stream_set_max_buffer_size(output, 0); + if (o_stream_send_istream(output, req->input) < 0) + ret = -1; + o_stream_set_max_buffer_size(output, (size_t)-1); + + if (req->input->eof) { + if (req->input->v_offset != req->input_size) { + i_error("stream input size changed"); //FIXME + return -1; + } + i_stream_unref(&req->input); + req->state = HTTP_REQUEST_STATE_WAITING; + conn->output_locked = FALSE; + http_client_request_debug(req, "Sent all payload"); + + } else { + conn->output_locked = TRUE; + o_stream_set_flush_pending(output, TRUE); + http_client_request_debug(req, "Partially sent payload"); + } + return ret; +} + +int http_client_request_send(struct http_client_request *req) +{ + struct http_client_connection *conn = req->conn; + struct ostream *output = conn->conn.output; + string_t *rtext = t_str_new(256); + struct const_iovec iov[3]; + int ret = 0; + + i_assert(!req->conn->output_locked); + + str_append(rtext, req->method); + str_append(rtext, " "); + str_append(rtext, req->target); + str_append(rtext, " HTTP/1.1\r\n"); + str_append(rtext, "Host: "); + str_append(rtext, req->hostname); + if ((!req->ssl &&req->port != HTTP_DEFAULT_PORT) || + (req->ssl && req->port != HTTPS_DEFAULT_PORT)) { + str_printfa(rtext, ":%u", req->port); + } + str_append(rtext, "\r\n"); + if (req->payload_sync) { + str_append(rtext, "Expect: 100-continue\r\n"); + } + if (req->input_size != 0) { + str_printfa(rtext, "Content-Length: %"PRIuUOFF_T"\r\n", + req->input_size); + } + + iov[0].iov_base = str_data(rtext); + iov[0].iov_len = str_len(rtext); + iov[1].iov_base = str_data(req->headers); + iov[1].iov_len = str_len(req->headers); + iov[2].iov_base = "\r\n"; + iov[2].iov_len = 2; + + req->state = HTTP_REQUEST_STATE_PAYLOAD_OUT; + o_stream_cork(output); + if (o_stream_sendv(output, iov, N_ELEMENTS(iov)) < 0) + ret = -1; + + http_client_request_debug(req, "Sent"); + + if (ret >= 0 && req->input_size != 0) { + if (!req->payload_sync) { + if (http_client_request_send_more(req) < 0) + ret = -1; + } else { + http_client_request_debug(req, "Waiting for 100-continue"); + } + } else { + req->state = HTTP_REQUEST_STATE_WAITING; + conn->output_locked = FALSE; + } + o_stream_uncork(output); + return ret; +} + +void http_client_request_callback(struct http_client_request *req, + struct http_response *response) +{ + http_client_request_callback_t *callback = req->callback; + + req->state = HTTP_REQUEST_STATE_GOT_RESPONSE; + + req->callback = NULL; + if (callback != NULL) + callback(response, req->context); +} + +static void +http_client_request_send_error(struct http_client_request *req, + unsigned int status, const char *error) +{ + http_client_request_callback_t *callback; + + req->state = HTTP_REQUEST_STATE_ABORTED; + + callback = req->callback; + req->callback = NULL; + if (callback != NULL) { + struct http_response response; + + memset(&response, 0, sizeof(response)); + response.status = status; + response.reason = error; + (void)callback(&response, req->context); + } +} + +void http_client_request_error(struct http_client_request *req, + unsigned int status, const char *error) +{ + http_client_request_send_error(req, status, error); + http_client_request_unref(&req); +} + +void http_client_request_abort(struct http_client_request **_req) +{ + struct http_client_request *req = *_req; + + if (req->state >= HTTP_REQUEST_STATE_FINISHED) + return; + req->callback = NULL; + req->state = HTTP_REQUEST_STATE_ABORTED; + if (req->host != NULL) + http_client_host_drop_request(req->host, req); + http_client_request_unref(_req); +} + +void http_client_request_finish(struct http_client_request **_req) +{ + struct http_client_request *req = *_req; + + if (req->state >= HTTP_REQUEST_STATE_FINISHED) + return; + + http_client_request_debug(req, "Finished"); + + req->callback = NULL; + req->state = HTTP_REQUEST_STATE_FINISHED; + http_client_request_unref(_req); +} + +void http_client_request_redirect(struct http_client_request *req, + const char *location) +{ + struct http_url *url; + const char *error; + unsigned int newport; + + /* parse URL */ + if (http_url_parse(location, NULL, 0, &url, &error) < 0) { + http_client_request_error(req, HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT, + t_strdup_printf("Invalid redirect location: %s", error)); + return; + } + + if (++req->redirects > req->client->set.max_redirects) { + if (req->client->set.max_redirects > 0) { + http_client_request_error(req, + HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT, + t_strdup_printf("Redirected more than %d times", + req->client->set.max_redirects)); + } else { + http_client_request_error(req, + HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT, + "Redirect refused"); + } + return; + } + + newport = (url->have_port ? url->port : (url->have_ssl ? 443 : 80)); + + http_client_request_debug(req, "Redirecting to http://%s:%u%s", + url->host_name, newport, url->path); + + // FIXME: handle literal IP specially (avoid duplicate parsing) + req->host = NULL; + req->conn = NULL; + req->hostname = p_strdup(req->pool, url->host_name); + req->port = newport; + req->target = p_strdup(req->pool, url->path); + req->ssl = url->have_ssl; + + /* resubmit */ + req->client->pending_requests--; + req->state = HTTP_REQUEST_STATE_NEW; + http_client_request_submit(req); +} + +void http_client_request_resubmit(struct http_client_request *req) +{ + http_client_request_debug(req, "Resubmitting request"); + + req->conn = NULL; + req->peer = NULL; + req->state = HTTP_REQUEST_STATE_QUEUED; + http_client_host_submit_request(req->host, req); +} + +void http_client_request_retry(struct http_client_request *req, + unsigned int status, const char *error) +{ + /* limit the number of attempts for each request */ + if (++req->attempts >= req->client->set.max_attempts) { + /* return error */ + http_client_request_error(req, status, error); + return; + } + + http_client_request_debug(req, "Retrying (attempts=%d)", req->attempts); + + /* resubmit */ + http_client_request_resubmit(req); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,182 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "hash.h" +#include "array.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "connection.h" +#include "dns-lookup.h" +#include "iostream-rawlog.h" +#include "iostream-ssl.h" +#include "http-url.h" +#include "http-response-parser.h" + +#include "http-client-private.h" + +#define HTTP_DEFAULT_PORT 80 +#define HTTPS_DEFAULT_PORT 443 + +/* FIXME: This implementation not yet finished. The essence works: it is + possible to submit requests through the client. Responses are dumped to + stdout + + Structure so far: + Client - Acts much like a browser; it is not dedicated to a single host. + Client can accept requests to different hosts, which can be served + at different IPs. Redirects can be handled in the background by + making a new connection. Connections to new hosts are created once + needed for servicing a request. + Requests - Semantics are similar to imapc commands. Create a request, + optionally modify some aspects of it and finally submit it. + Hosts - We maintain a 'cache' of hosts for which we have looked up IPs. + Requests are first queued in the host struct on a per-port basis. + Peers - Group connections to the same ip/port (== peer_addr). + Connections - Actual connections to a server. Once a connection is ready to + handle requests, it claims a request from a host object. One + connection hand service multiple hosts and one host can have + multiple associated connections, possibly to different ips and + ports. + + TODO: lots of cleanup, authentication, ssl, timeouts, rawlog etc. + */ + +/* + * Logging + */ + +static inline void +http_client_debug(struct http_client *client, + const char *format, ...) ATTR_FORMAT(2, 3); + +static inline void +http_client_debug(struct http_client *client, + const char *format, ...) +{ + va_list args; + + va_start(args, format); + if (client->set.debug) + i_debug("http-client: %s", t_strdup_vprintf(format, args)); + va_end(args); +} + +/* + * Client + */ + +struct http_client *http_client_init(const struct http_client_settings *set) +{ + struct http_client *client; + pool_t pool; + + pool = pool_alloconly_create("http client", 1024); + client = p_new(pool, struct http_client, 1); + client->pool = pool; + client->set.dns_client_socket_path = + p_strdup(pool, set->dns_client_socket_path); + client->set.rawlog_dir = p_strdup(pool, set->rawlog_dir); + client->set.ssl_ca_dir = p_strdup(pool, set->ssl_ca_dir); + client->set.max_idle_time_msecs = set->max_idle_time_msecs; + client->set.max_parallel_connections = + (set->max_parallel_connections > 0 ? set->max_parallel_connections : 1); + client->set.max_pipelined_requests = + (set->max_pipelined_requests > 0 ? set->max_pipelined_requests : 1); + client->set.max_attempts = set->max_attempts; + client->set.max_redirects = set->max_redirects; + client->set.debug = set->debug; + + client->conn_list = http_client_connection_list_init(); + + hash_table_create(&client->hosts, default_pool, 0, str_hash, strcmp); + hash_table_create(&client->peers, default_pool, 0, + http_client_peer_addr_hash, http_client_peer_addr_cmp); + + return client; +} + +void http_client_deinit(struct http_client **_client) +{ + struct http_client *client = *_client; + struct hash_iterate_context *iter; + const char *hostname; + struct http_client_host *host; + const struct http_client_peer_addr *addr; + struct http_client_peer *peer; + + /* free peers */ + iter = hash_table_iterate_init(client->peers); + while (hash_table_iterate(iter, client->peers, &addr, &peer)) { + http_client_peer_free(&peer); + } + hash_table_iterate_deinit(&iter); + hash_table_destroy(&client->peers); + + /* free hosts */ + iter = hash_table_iterate_init(client->hosts); + while (hash_table_iterate(iter, client->hosts, &hostname, &host)) { + http_client_host_free(&host); + } + hash_table_iterate_deinit(&iter); + hash_table_destroy(&client->hosts); + + connection_list_deinit(&client->conn_list); + pool_unref(&client->pool); + *_client = NULL; +} + +void http_client_switch_ioloop(struct http_client *client) +{ + struct connection *_conn = client->conn_list->connections; + + /* move connections */ + /* FIXME: we wouldn't necessarily need to switch all of them + immediately, only those that have requests now. but also connections + that get new requests before ioloop is switched again.. */ + for (; _conn != NULL; _conn = _conn->next) { + struct http_client_connection *conn = + (struct http_client_connection *)_conn; + + http_client_connection_switch_ioloop(conn); + } + + /* move dns lookups */ + if (client->set.dns_client_socket_path != '\0') { + struct hash_iterate_context *iter; + struct http_client_host *host; + const char *hostname; + + iter = hash_table_iterate_init(client->hosts); + while (hash_table_iterate(iter, client->hosts, &hostname, &host)) { + http_client_host_switch_ioloop(host); + } + hash_table_iterate_deinit(&iter); + } +} + +void http_client_wait(struct http_client *client) +{ + struct ioloop *prev_ioloop = current_ioloop; + + i_assert(client->ioloop == NULL); + + client->ioloop = io_loop_create(); + http_client_switch_ioloop(client); + + do { + http_client_debug(client, + "Waiting for %d requests to finish", client->pending_requests); + io_loop_run(client->ioloop); + } while (client->pending_requests > 0); + + http_client_debug(client, "All requests finished"); + + current_ioloop = prev_ioloop; + http_client_switch_ioloop(client); + current_ioloop = client->ioloop; + io_loop_destroy(&client->ioloop); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-client.h Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,83 @@ +#ifndef HTTP_CLIENT_H +#define HTTP_CLIENT_H + +struct http_response; + +struct http_client; +struct http_client_request; + +enum http_client_request_error { + HTTP_CLIENT_REQUEST_ERROR_ABORTED = 9000, + HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED, + HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED, + HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT, + HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, + HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE, + HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT, +}; + +struct http_client_settings { + const char *dns_client_socket_path; + + const char *ssl_ca_dir; + const char *ssl_crypto_device; + bool ssl_allow_invalid_cert; + bool ssl_verify; + + const char *rawlog_dir; + + unsigned int max_idle_time_msecs; + + /* maximum number of parallel connections per peer (default = 1) */ + unsigned int max_parallel_connections; + + /* maximum number of pipelined requests per connection (default = 1) */ + unsigned int max_pipelined_requests; + + /* maximum number of redirects for a request + (default = 0; redirects refused) + */ + unsigned int max_redirects; + + /* maximum number of attempts for a request */ + unsigned int max_attempts; + + bool debug; +}; + +typedef void +http_client_request_callback_t(const struct http_response *response, + void *context); + +struct http_client *http_client_init(const struct http_client_settings *set); +void http_client_deinit(struct http_client **_client); + +struct http_client_request * +http_client_request(struct http_client *client, + const char *method, const char *host, const char *target, + http_client_request_callback_t *callback, void *context); +#define http_client_request(client, method, host, target, callback, context) \ + http_client_request(client, method, host, target + \ + CALLBACK_TYPECHECK(callback, void (*)( \ + const struct http_response *response, typeof(context))), \ + (http_client_request_callback_t *)callback, context) + +void http_client_request_set_port(struct http_client_request *req, + unsigned int port); +void http_client_request_set_ssl(struct http_client_request *req, + bool ssl); +void http_client_request_set_urgent(struct http_client_request *req); + +void http_client_request_add_header(struct http_client_request *req, + const char *key, const char *value); +void http_client_request_set_payload(struct http_client_request *req, + struct istream *input, bool sync); +void http_client_request_submit(struct http_client_request *req); +void http_client_request_abort(struct http_client_request **req); + +void http_client_switch_ioloop(struct http_client *client); + +/* blocks until all currently submitted requests are handled */ +void http_client_wait(struct http_client *client); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-header-parser.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,282 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "buffer.h" +#include "istream.h" +#include "str.h" +#include "str-sanitize.h" +#include "http-parser.h" + +#include "http-header-parser.h" + +enum http_header_parse_state { + HTTP_HEADER_PARSE_STATE_INIT = 0, + HTTP_HEADER_PARSE_STATE_NAME, + HTTP_HEADER_PARSE_STATE_COLON, + HTTP_HEADER_PARSE_STATE_OWS, + HTTP_HEADER_PARSE_STATE_CONTENT, + HTTP_HEADER_PARSE_STATE_CR, + HTTP_HEADER_PARSE_STATE_LF, + HTTP_HEADER_PARSE_STATE_NEW_LINE, + HTTP_HEADER_PARSE_STATE_LAST_LINE, + HTTP_HEADER_PARSE_STATE_EOH +}; + +struct http_header_parser { + struct istream *input; + + const unsigned char *begin, *cur, *end; + const char *error; + + string_t *name; + buffer_t *value_buf; + + enum http_header_parse_state state; +}; + +// FIXME(Stephan): Add support for limiting maximum header size. + +struct http_header_parser *http_header_parser_init(struct istream *input) +{ + struct http_header_parser *parser; + + parser = i_new(struct http_header_parser, 1); + parser->input = input; + parser->name = str_new(default_pool, 128); + parser->value_buf = buffer_create_dynamic(default_pool, 4096); + + return parser; +} + +void http_header_parser_deinit(struct http_header_parser **_parser) +{ + struct http_header_parser *parser = *_parser; + + //i_stream_skip(ctx->input, ctx->skip); + buffer_free(&parser->value_buf); + str_free(&parser->name); + i_free(parser); + *_parser = NULL; +} + +void http_header_parser_reset(struct http_header_parser *parser) +{ + parser->state = HTTP_HEADER_PARSE_STATE_INIT; +} + +static int http_header_parse_name(struct http_header_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* field-name = token + token = 1*tchar + */ + while (parser->cur < parser->end && http_char_is_token(*parser->cur)) + parser->cur++; + + str_append_n(parser->name, first, parser->cur-first); + + if (parser->cur == parser->end) + return 0; + if (str_len(parser->name) == 0) { + parser->error = "Empty header field name"; + return -1; + } + return 1; +} + +static int http_header_parse_ows(struct http_header_parser *parser) +{ + /* OWS = *( SP / HTAB ) + ; "optional" whitespace + */ + while (parser->cur < parser->end && + (*parser->cur == ' ' || *parser->cur == '\t')) + parser->cur++; + return (parser->cur == parser->end ? 0 : 1); +} + +static int http_header_parse_content(struct http_header_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* field-content = *( HTAB / SP / VCHAR / obs-text ) + */ + while (parser->cur < parser->end && http_char_is_text(*parser->cur)) + parser->cur++; + + buffer_append(parser->value_buf, first, parser->cur-first); + + if (parser->cur == parser->end) + return 0; + return 1; +} + +static inline const char *_chr_sanitize(unsigned char c) +{ + if (c >= 0x20 && c < 0x7F) + return t_strdup_printf("'%c'", c); + return t_strdup_printf("0x%02x", c); +} + +static int http_header_parse(struct http_header_parser *parser) +{ + int ret; + + /* 'header' = *( header-field CRLF ) CRLF + header-field = field-name ":" OWS field-value BWS + field-name = token + field-value = *( field-content / obs-fold ) + field-content = *( HTAB / SP / VCHAR / obs-text ) + obs-fold = CRLF ( SP / HTAB ) + ; obsolete line folding + ; see Section 3.2.2 + */ + + for (;;) { + switch (parser->state) { + case HTTP_HEADER_PARSE_STATE_INIT: + buffer_set_used_size(parser->value_buf, 0); + str_truncate(parser->name, 0); + parser->state = HTTP_HEADER_PARSE_STATE_NAME; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_NAME: + if (http_char_is_token(*parser->cur)) { + if ((ret=http_header_parse_name(parser)) <= 0) + return ret; + } else if (str_len(parser->name) == 0) { + parser->state = HTTP_HEADER_PARSE_STATE_LAST_LINE; + break; + } + parser->state = HTTP_HEADER_PARSE_STATE_COLON; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_COLON: + if (*parser->cur != ':') { + parser->error = t_strdup_printf + ("Expected ':' after header field name '%s', but found %s", + str_sanitize(str_c(parser->name),64), + _chr_sanitize(*parser->cur)); + return -1; + } + parser->cur++; + if (str_len(parser->name) == 0) { + parser->error = "Empty header field name"; + return -1; + } + parser->state = HTTP_HEADER_PARSE_STATE_OWS; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_OWS: + if ((ret=http_header_parse_ows(parser)) <= 0) + return ret; + parser->state = HTTP_HEADER_PARSE_STATE_CONTENT; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_CONTENT: + if ((ret=http_header_parse_content(parser)) <= 0) + return ret; + parser->state = HTTP_HEADER_PARSE_STATE_CR; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_CR: + if (*parser->cur == '\r') { + parser->cur++; + } + parser->state = HTTP_HEADER_PARSE_STATE_LF; + if (parser->cur == parser->end) + return 0; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_LF: + if (*parser->cur != '\n') { + parser->error = t_strdup_printf + ("Expected line end after header field '%s', but found %s", + str_sanitize(str_c(parser->name),64), + _chr_sanitize(*parser->cur)); + return -1; + } + parser->cur++; + parser->state = HTTP_HEADER_PARSE_STATE_NEW_LINE; + if (parser->cur == parser->end) + return 0; + /* fall through */ + case HTTP_HEADER_PARSE_STATE_NEW_LINE: + if (*parser->cur == ' ' || *parser->cur == '\t') { + /* obs-fold */ + buffer_append_c(parser->value_buf, ' '); + parser->state = HTTP_HEADER_PARSE_STATE_OWS; + break; + } + parser->state = HTTP_HEADER_PARSE_STATE_NAME; + return 1; + case HTTP_HEADER_PARSE_STATE_LAST_LINE: + if (*parser->cur == '\r') { + /* last CRLF */ + parser->cur++; + parser->state = HTTP_HEADER_PARSE_STATE_EOH; + if (parser->cur == parser->end) + return 0; + break; + } else if (*parser->cur == '\n') { + /* header fully parsed */ + parser->cur++; + parser->state = HTTP_HEADER_PARSE_STATE_EOH; + return 1; + } + parser->error = t_strdup_printf + ("Expected CRLF or header field name, but found %s", + _chr_sanitize(*parser->cur)); + return -1; + case HTTP_HEADER_PARSE_STATE_EOH: + if (*parser->cur != '\n') { + parser->error = t_strdup_printf + ("Expected LF after CR at end of header, but found %s", + _chr_sanitize(*parser->cur)); + return -1; + } + /* header fully parsed */ + parser->cur++; + return 1; + + default: + i_unreached(); + } + } + + i_unreached(); + return -1; +} + +int http_header_parse_next_field(struct http_header_parser *parser, + const char **name_r, const unsigned char **data_r, size_t *size_r, + const char **error_r) +{ + size_t size; + int ret; + + while ((ret=i_stream_read_data + (parser->input, &parser->begin, &size, 0)) > 0) { + parser->cur = parser->begin; + parser->end = parser->cur + size; + + if ((ret=http_header_parse(parser)) < 0) { + *error_r = parser->error; + return -1; + } + + i_stream_skip(parser->input, parser->cur - parser->begin); + + if (ret == 1) { + if (parser->state != HTTP_HEADER_PARSE_STATE_EOH) { + *name_r = str_c(parser->name); + *data_r = buffer_get_data(parser->value_buf, size_r); + parser->state = HTTP_HEADER_PARSE_STATE_INIT; + } else { + *name_r = NULL; + *data_r = NULL; + } + return 1; + } + } + + i_assert(ret != -2); + if (ret < 0) + *error_r = "Stream error"; + return ret; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-header-parser.h Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,15 @@ +#ifndef HTTP_HEADER_PARSER_H +#define HTTP_HEADER_PARSER_H + +struct http_header_parser; + +struct http_header_parser *http_header_parser_init(struct istream *input); +void http_header_parser_deinit(struct http_header_parser **_parser); + +void http_header_parser_reset(struct http_header_parser *parser); + +int http_header_parse_next_field(struct http_header_parser *parser, + const char **name_r, const unsigned char **data_r, size_t *size_r, + const char **error_r); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-parser.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,132 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "strescape.h" +#include "http-url.h" + +#include "http-parser.h" + +/* + Character definitions: + + tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + / DIGIT / ALPHA + ; any VCHAR, except special + special = "(" / ")" / "<" / ">" / "@" / "," + / ";" / ":" / "\" / DQUOTE / "/" / "[" + / "]" / "?" / "=" / "{" / "}" + qdtext = OWS / %x21 / %x23-5B / %x5D-7E / obs-text + qdtext-nf = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + ctext = OWS / %x21-27 / %x2A-5B / %x5D-7E / obs-text + obs-text = %x80-FF + OWS = *( SP / HTAB ) + + Mapping + (1<<0) => tchar + (1<<1) => special + (1<<2) => %x21 / %x2A-5B / %x5D-7E + (1<<3) => %x23-29 + (1<<4) => %x22-27 + (1<<5) => HTAB / SP / obs-text + */ + +const unsigned char _http_token_char_mask = (1<<0); +const unsigned char _http_value_char_mask = (1<<0)|(1<<1); +const unsigned char _http_text_char_mask = (1<<0)|(1<<1)|(1<<5); +const unsigned char _http_qdtext_char_mask = (1<<2)|(1<<3)|(1<<5); +const unsigned char _http_ctext_char_mask = (1<<2)|(1<<4)|(1<<5); + +const unsigned char _http_char_lookup[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, // 00 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 + 32, 21, 18, 25, 25, 25, 25, 25, 10, 10, 5, 5, 6, 5, 5, 6, // 20 + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, // 30 + 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 40 + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 2, 6, 5, 5, // 50 + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 60 + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 6, 5, 0, // 70 + + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // 80 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // 90 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // A0 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // B0 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // C0 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // D0 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // E0 + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // F0 +}; + +/* + * HTTP value parsing + */ + +void http_parser_init(struct http_parser *parser, + const unsigned char *data, size_t size) +{ + memset(parser, 0, sizeof(*parser)); + parser->begin = data; + parser->cur = data; + parser->end = data + size; +} + +void http_parse_ows(struct http_parser *parser) +{ + /* OWS = *( SP / HTAB ) */ + if (parser->cur >= parser->end) + return; + while (parser->cur < parser->end && + (parser->cur[0] == ' ' || parser->cur[0] == '\t')) { + parser->cur++; + } +} + +int http_parse_token(struct http_parser *parser, const char **token_r) +{ + const unsigned char *first; + + /* token = 1*tchar */ + + if (parser->cur >= parser->end || !http_char_is_token(*parser->cur)) + return 0; + + first = parser->cur++; + while (parser->cur < parser->end && http_char_is_token(*parser->cur)) + parser->cur++; + + *token_r = t_strndup(first, parser->cur - first); + return 1; +} + +int http_parse_token_list_next(struct http_parser *parser, + const char **token_r) +{ + /* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-21; + Appendix B: + + For compatibility with legacy list rules, recipients SHOULD accept + empty list elements. In other words, consumers would follow the list + productions: + + #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ] + 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] ) + */ + + for (;;) { + if (http_parse_token(parser, token_r) > 0) + break; + http_parse_ows(parser); + if (parser->cur >= parser->end || parser->cur[0] != ',') + return 0; + parser->cur++; + http_parse_ows(parser); + } + + return 1; +} + + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-parser.h Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,53 @@ +#ifndef HTTP_PARSER_H +#define HTTP_PARSER_H + +/* + * Character definitions + */ + +extern const unsigned char _http_token_char_mask; +extern const unsigned char _http_value_char_mask; +extern const unsigned char _http_text_char_mask; +extern const unsigned char _http_qdtext_char_mask; +extern const unsigned char _http_ctext_char_mask; + +extern const unsigned char _http_char_lookup[256]; + +static inline bool http_char_is_token(unsigned char ch) { + return (_http_char_lookup[ch] & _http_token_char_mask) != 0; +} + +static inline bool http_char_is_value(unsigned char ch) { + return (_http_char_lookup[ch] & _http_value_char_mask) != 0; +} + +static inline bool http_char_is_text(unsigned char ch) { + return (_http_char_lookup[ch] & _http_text_char_mask) != 0; +} + +static inline bool http_char_is_qdtext(unsigned char ch) { + return (_http_char_lookup[ch] & _http_qdtext_char_mask) != 0; +} + +static inline bool http_char_is_ctext(unsigned char ch) { + return (_http_char_lookup[ch] & _http_ctext_char_mask) != 0; +} + +/* + * HTTP value parsing + */ + +struct http_parser { + const unsigned char *begin, *cur, *end; +}; + +void http_parser_init(struct http_parser *parser, + const unsigned char *data, size_t size); + +void http_parse_ows(struct http_parser *parser); + +int http_parse_token(struct http_parser *parser, const char **token_r); +int http_parse_token_list_next(struct http_parser *parser, + const char **token_r); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-response-parser.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,503 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "strfuncs.h" +#include "istream.h" +#include "http-parser.h" +#include "http-date.h" +#include "http-header-parser.h" +#include "http-transfer.h" + +#include "http-response-parser.h" + +#include <ctype.h> + +enum http_response_parser_state { + HTTP_RESPONSE_PARSE_STATE_INIT = 0, + HTTP_RESPONSE_PARSE_STATE_VERSION, + HTTP_RESPONSE_PARSE_STATE_SP1, + HTTP_RESPONSE_PARSE_STATE_STATUS, + HTTP_RESPONSE_PARSE_STATE_SP2, + HTTP_RESPONSE_PARSE_STATE_REASON, + HTTP_RESPONSE_PARSE_STATE_CR, + HTTP_RESPONSE_PARSE_STATE_LF, + HTTP_RESPONSE_PARSE_STATE_HEADER +}; + +struct http_response_parser { + struct istream *input; + + const unsigned char *begin, *cur, *end; + const char *error; + + string_t *strbuf; + + enum http_response_parser_state state; + struct http_header_parser *header_parser; + + uoff_t content_length; + const char *transfer_encoding; + struct istream *payload; + + struct http_response *response; + pool_t response_pool; +}; + +struct http_response_parser *http_response_parser_init(struct istream *input) +{ + struct http_response_parser *parser; + + parser = i_new(struct http_response_parser, 1); + parser->input = input; + parser->strbuf = str_new(default_pool, 128); + return parser; +} + +void http_response_parser_deinit(struct http_response_parser **_parser) +{ + struct http_response_parser *parser = *_parser; + + str_free(&parser->strbuf); + if (parser->header_parser != NULL) + http_header_parser_deinit(&parser->header_parser); + if (parser->response_pool != NULL) + pool_unref(&parser->response_pool); + if (parser->payload != NULL) + i_stream_unref(&parser->payload); + i_free(parser); +} + +static void +http_response_parser_restart(struct http_response_parser *parser) +{ + i_assert(parser->payload == NULL); + parser->content_length = 0; + parser->transfer_encoding = NULL; + str_truncate(parser->strbuf, 0); + if (parser->response_pool != NULL) + pool_unref(&parser->response_pool); + parser->response_pool = pool_alloconly_create("http_response", 2048); + parser->response = p_new(parser->response_pool, struct http_response, 1); + parser->response->date = (time_t)-1; + p_array_init(&parser->response->headers, parser->response_pool, 32); +} + +static int http_response_parse_version(struct http_response_parser *parser) +{ + const unsigned char *first = parser->cur; + const char *p; + + /* HTTP-version = HTTP-name "/" DIGIT "." DIGIT + HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive + */ + while (parser->cur < parser->end && http_char_is_value(*parser->cur)) + parser->cur++; + + if (str_len(parser->strbuf) + (parser->cur-first) > 8) + return -1; + + str_append_n(parser->strbuf, first, parser->cur-first); + if (parser->cur == parser->end) + return 0; + + if (str_len(parser->strbuf) != 8) + return -1; + if (strncmp(str_c(parser->strbuf), "HTTP/",5) != 0) + return -1; + p = str_c(parser->strbuf) + 5; + if (!i_isdigit(*p)) + return -1; + parser->response->version_major = *p - '0'; + p++; + if (*(p++) != '.') + return -1; + if (!i_isdigit(*p)) + return -1; + parser->response->version_minor = *p - '0'; + return 1; +} + +static int http_response_parse_status(struct http_response_parser *parser) +{ + const unsigned char *first = parser->cur; + const char *p; + + /* status-code = 3DIGIT + */ + while (parser->cur < parser->end && i_isdigit(*parser->cur)) + parser->cur++; + + if (str_len(parser->strbuf) + (parser->cur-first) > 3) + return -1; + + str_append_n(parser->strbuf, first, parser->cur-first); + if (parser->cur == parser->end) + return 0; + if (str_len(parser->strbuf) != 3) + return -1; + p = str_c(parser->strbuf); + parser->response->status = + (p[0] - '0')*100 + (p[1] - '0')*10 + (p[2] - '0'); + return 1; +} + +static int http_response_parse_reason(struct http_response_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + */ + while (parser->cur < parser->end && http_char_is_text(*parser->cur)) + parser->cur++; + + str_append_n(parser->strbuf, first, parser->cur-first); + + if (parser->cur == parser->end) + return 0; + parser->response->reason = + p_strdup(parser->response_pool, str_c(parser->strbuf)); + return 1; +} + +static inline const char *_chr_sanitize(unsigned char c) +{ + if (c >= 0x20 && c < 0x7F) + return t_strdup_printf("'%c'", c); + return t_strdup_printf("0x%02x", c); +} + +static int http_response_parse(struct http_response_parser *parser) +{ + int ret; + + /* status-line = HTTP-version SP status-code SP reason-phrase CRLF + status-code = 3DIGIT + reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + */ + + for (;;) { + switch (parser->state) { + case HTTP_RESPONSE_PARSE_STATE_INIT: + http_response_parser_restart(parser); + parser->state = HTTP_RESPONSE_PARSE_STATE_VERSION; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_VERSION: + if ((ret=http_response_parse_version(parser)) <= 0) { + if (ret < 0) + parser->error = "Invalid HTTP version in response"; + return ret; + } + str_truncate(parser->strbuf, 0); + parser->state = HTTP_RESPONSE_PARSE_STATE_SP1; + if (parser->cur == parser->end) + return 0; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_SP1: + if (*parser->cur != ' ') { + parser->error = t_strdup_printf + ("Expected ' ' after response version, but found %s", + _chr_sanitize(*parser->cur)); + return -1; + } + parser->cur++; + str_truncate(parser->strbuf, 0); + parser->state = HTTP_RESPONSE_PARSE_STATE_STATUS; + if (parser->cur >= parser->end) + return 0; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_STATUS: + if ((ret=http_response_parse_status(parser)) <= 0) { + if (ret < 0) + parser->error = "Invalid HTTP status code in response"; + return ret; + } + str_truncate(parser->strbuf, 0); + parser->state = HTTP_RESPONSE_PARSE_STATE_SP2; + if (parser->cur == parser->end) + return 0; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_SP2: + if (*parser->cur != ' ') { + parser->error = t_strdup_printf + ("Expected ' ' after response status code, but found %s", + _chr_sanitize(*parser->cur)); + return -1; + } + parser->cur++; + str_truncate(parser->strbuf, 0); + parser->state = HTTP_RESPONSE_PARSE_STATE_REASON; + if (parser->cur >= parser->end) + return 0; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_REASON: + if ((ret=http_response_parse_reason(parser)) <= 0) + return ret; + str_truncate(parser->strbuf, 0); + parser->state = HTTP_RESPONSE_PARSE_STATE_CR; + if (parser->cur == parser->end) + return 0; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_CR: + if (*parser->cur == '\r') + parser->cur++; + parser->state = HTTP_RESPONSE_PARSE_STATE_LF; + if (parser->cur == parser->end) + return 0; + /* fall through */ + case HTTP_RESPONSE_PARSE_STATE_LF: + if (*parser->cur != '\n') { + parser->error = t_strdup_printf + ("Expected line end after response, but found %s", + _chr_sanitize(*parser->cur)); + return -1; + } + parser->cur++; + parser->state = HTTP_RESPONSE_PARSE_STATE_HEADER; + return 1; + case HTTP_RESPONSE_PARSE_STATE_HEADER: + default: + i_unreached(); + } + } + + i_unreached(); + return -1; +} + +static int http_response_parse_status_line(struct http_response_parser *parser) +{ + size_t size; + int ret; + + while ((ret = i_stream_read_data(parser->input, + &parser->begin, &size, 0)) > 0) { + parser->cur = parser->begin; + parser->end = parser->cur + size; + + if ((ret = http_response_parse(parser)) < 0) + return -1; + + i_stream_skip(parser->input, parser->cur - parser->begin); + if (ret > 0) + return 1; + } + + i_assert(ret != -2); + if (ret < 0) { + if (parser->input->eof && parser->state == HTTP_RESPONSE_PARSE_STATE_INIT) + return 0; + parser->error = "Stream error"; + return -1; + } + return 0; +} + +static int +http_response_parse_header(struct http_response_parser *parser, + const char *name, const unsigned char *data, size_t size) +{ + struct http_response_header *hdr; + struct http_parser hparser; + + hdr = array_append_space(&parser->response->headers); + hdr->key = p_strdup(parser->response_pool, name); + hdr->value = p_strndup(parser->response_pool, data, size); + hdr->size = size; + + switch (name[0]) { + case 'C': case 'c': + if (strcasecmp(name, "Connection") == 0) { + const char *option; + + /* Connection = 1#connection-option + connection-option = token + */ + http_parser_init(&hparser, data, size); + for (;;) { + if (http_parse_token_list_next(&hparser, &option) <= 0) + break; + if (strcasecmp(option, "close") == 0) { + parser->response->connection_close = TRUE; + break; // not interested in any other options + } + } + return 1; + } + if (strcasecmp(name, "Content-Length") == 0) { + /* Content-Length = 1*DIGIT */ + if (str_to_uoff(hdr->value, &parser->content_length) < 0) { + parser->error = "Invalid Content-Length header"; + return -1; + } + return 1; + } + break; + case 'D': case 'd': + if (strcasecmp(name, "Date") == 0) { + /* Date = HTTP-date */ + (void)http_date_parse(data, size, &parser->response->date); + return 1; + } + break; + case 'L': case 'l': + if (strcasecmp(name, "Location") == 0) { + /* Location = URI-reference (not parsed here) */ + parser->response->location = + p_strndup(parser->response_pool, data, size); + return 1; + } + break; + case 'T': case 't': + if (strcasecmp(name, "Transfer-Encoding") == 0) { + /* Transfer-Encoding = 1#transfer-coding */ + parser->transfer_encoding = hdr->value; + return 1; + } + break; + default: + break; + } + return 1; +} + +int http_response_parse_next(struct http_response_parser *parser, + bool no_payload, struct http_response **response_r, + const char **error_r) +{ + struct http_parser hparser; + const char *field_name, *error; + const unsigned char *field_data; + size_t field_size; + int ret; + + /* make sure we finished streaming payload from previous response + before we continue. */ + if (parser->payload != NULL) { + struct istream *payload = parser->payload; + + i_assert(parser->state == HTTP_RESPONSE_PARSE_STATE_INIT); + + if (!payload->eof) { + while ((ret=i_stream_read(payload)) > 0) { + i_stream_skip(payload, i_stream_get_data_size(payload)); + } + if (ret == 0) + return 0; + if (ret < 0 && !payload->eof) { + *error_r = "Stream error while skipping payload"; + return -1; + } + } + + if (payload->eof) { + i_stream_unref(&parser->payload); + parser->payload = NULL; + } + } + + /* HTTP-message = start-line + *( header-field CRLF ) + CRLF + [ message-body ] + */ + + /* start-line */ + if (parser->state != HTTP_RESPONSE_PARSE_STATE_HEADER) { + if ((ret=http_response_parse_status_line(parser)) <= 0) { + *error_r = parser->error; + return ret; + } + } + + /* *( header-field CRLF ) CRLF */ + if (parser->header_parser == NULL) + parser->header_parser = http_header_parser_init(parser->input); + else + http_header_parser_reset(parser->header_parser); + + while ((ret=http_header_parse_next_field + (parser->header_parser, &field_name, &field_data, &field_size, &error)) > 0) { + if (field_name == NULL) break; + if ((ret=http_response_parse_header + (parser, field_name, field_data, field_size)) < 0) { + *error_r = parser->error; + return -1; + } + } + + if (ret <= 0) { + if (ret < 0) + *error_r = t_strdup_printf("Failed to parse response header: %s", error); + return ret; + } + + /* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-21 + Section 3.3.2: + + A server MUST NOT send a Content-Length header field in any response + with a status code of 1xx (Informational) or 204 (No Content). [...] + */ + if ((parser->response->status / 100 == 1 || + parser->response->status == 204) && parser->content_length > 0) { + *error_r = t_strdup_printf( + "Unexpected Content-Length header field for %u response " + "(length=%"PRIuUOFF_T")", parser->response->status, + parser->content_length); + return -1; + } + + /* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-21 + Section 3.3.3: + + Any response to a HEAD request and any response with a 1xx + (Informational), 204 (No Content), or 304 (Not Modified) status + code is always terminated by the first empty line after the + header fields, regardless of the header fields present in the + message, and thus cannot contain a message body. + */ + if (parser->response->status / 100 == 1 || parser->response->status == 204 + || parser->response->status == 304) { // HEAD is handled in caller + no_payload = TRUE; + } + + if (!no_payload) { + /* [ message-body ] */ + if (parser->content_length > 0) { + /* Got explicit message size from Content-Length: header */ + parser->payload = parser->response->payload = + i_stream_create_limit(parser->input, parser->content_length); + } else if (parser->transfer_encoding != NULL) { + const char *tenc; + + /* Transfer-Encoding = 1#transfer-coding + transfer-coding = "chunked" / "compress" / "deflate" / "gzip" + / transfer-extension ; [FIXME] + transfer-extension = token *( OWS ";" OWS transfer-parameter ) + */ + // FIXME: parse this directly when the header is parsed + http_parser_init(&hparser, + (const unsigned char *)parser->transfer_encoding, + strlen(parser->transfer_encoding)); + for (;;) { + if (http_parse_token_list_next(&hparser, &tenc) <= 0) + break; + if (strcasecmp(tenc, "chunked") == 0) { + parser->payload = parser->response->payload = + http_transfer_chunked_istream_create(parser->input); + break; // FIXME + } else { + *error_r = t_strdup_printf( + "Unkown Transfer-Encoding `%s' for %u response", + tenc, parser->response->status); + return -1; + } + } + } + } + + parser->state = HTTP_RESPONSE_PARSE_STATE_INIT; + *response_r = parser->response; + return 1; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-response-parser.h Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,37 @@ +#ifndef HTTP_RESPONSE_PARSER_H +#define HTTP_RESPONSE_PARSER_H + +struct http_response_header { + const char *key; + const char *value; + size_t size; +}; + +struct http_response { + unsigned char version_major; + unsigned char version_minor; + + unsigned int status; + + const char *reason; + const char *location; + + time_t date; + struct istream *payload; + + ARRAY(struct http_response_header) headers; + + unsigned int connection_close:1; +}; + +struct http_response_parser; + +struct http_response_parser * +http_response_parser_init(struct istream *input); +void http_response_parser_deinit(struct http_response_parser **_parser); + +int http_response_parse_next(struct http_response_parser *parser, + bool no_payload, struct http_response **response_r, + const char **error_r); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-transfer-chunked.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,479 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "istream-private.h" +#include "ostream-private.h" +#include "http-parser.h" +#include "http-header-parser.h" + +#include "http-transfer.h" + +/* + * Chunked input stream + */ + +enum http_transfer_chunked_parse_state { + HTTP_CHUNKED_PARSE_STATE_INIT, + HTTP_CHUNKED_PARSE_STATE_SIZE, + HTTP_CHUNKED_PARSE_STATE_EXT, + HTTP_CHUNKED_PARSE_STATE_EXT_NAME, + HTTP_CHUNKED_PARSE_STATE_EXT_EQ, + HTTP_CHUNKED_PARSE_STATE_EXT_VALUE, + HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING, + HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_ESCAPE, + HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_TOKEN, + HTTP_CHUNKED_PARSE_STATE_CR, + HTTP_CHUNKED_PARSE_STATE_LF, + HTTP_CHUNKED_PARSE_STATE_DATA, + HTTP_CHUNKED_PARSE_STATE_DATA_READY, + HTTP_CHUNKED_PARSE_STATE_DATA_CR, + HTTP_CHUNKED_PARSE_STATE_DATA_LF, + HTTP_CHUNKED_PARSE_STATE_TRAILER, + HTTP_CHUNKED_PARSE_STATE_FINISHED, +}; + +struct http_transfer_chunked_istream { + struct istream_private istream; + struct stat statbuf; + + const unsigned char *begin, *cur, *end; + enum http_transfer_chunked_parse_state state; + unsigned int parsed_chars; + + uoff_t chunk_size, chunk_v_offset, chunk_pos; + const char *error; + + struct http_header_parser *header_parser; + + unsigned int finished:1; +}; + +/* Chunk parser */ + +static inline const char *_chr_sanitize(unsigned char c) +{ + if (c >= 0x20 && c < 0x7F) + return t_strdup_printf("'%c'", c); + return t_strdup_printf("0x%02x", c); +} + +static int http_transfer_chunked_parse_size +(struct http_transfer_chunked_istream *tcstream) +{ + uoff_t size = 0, prev; + + /* chunk-size = 1*HEXDIG */ + + while (tcstream->cur < tcstream->end) { + prev = tcstream->chunk_size; + + if (*tcstream->cur >= '0' && *tcstream->cur <= '9') + size = *tcstream->cur-'0'; + else if (*tcstream->cur >= 'A' && *tcstream->cur <= 'F') + size = *tcstream->cur-'A' + 10; + else if (*tcstream->cur >= 'a' && *tcstream->cur <= 'f') + size = *tcstream->cur-'a' + 10; + else { + if (tcstream->parsed_chars == 0) { + tcstream->error = t_strdup_printf( + "Expected chunk size digit, but found %s", + _chr_sanitize(*tcstream->cur)); + return -1; + } + tcstream->parsed_chars = 0; + return 1; + } + tcstream->chunk_size <<= 4; + tcstream->chunk_size += size; + if (tcstream->chunk_size < prev) { + tcstream->error = "Chunk size exceeds integer limit"; + return -1; + } + tcstream->parsed_chars++; + tcstream->cur++; + } + + return 0; +} + +static int http_transfer_chunked_skip_token +(struct http_transfer_chunked_istream *tcstream) +{ + const unsigned char *first = tcstream->cur; + + /* token = 1*tchar */ + while (tcstream->cur < tcstream->end && http_char_is_token(*tcstream->cur)) + tcstream->cur++; + + tcstream->parsed_chars += (tcstream->cur-first); + if (tcstream->cur == tcstream->end) + return 0; + if (tcstream->parsed_chars == 0) + return -1; + return 1; +} + +static int http_transfer_chunked_skip_qdtext +(struct http_transfer_chunked_istream *tcstream) +{ + /* qdtext-nf = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text */ + while (tcstream->cur < tcstream->end && http_char_is_qdtext(*tcstream->cur)) + tcstream->cur++; + if (tcstream->cur == tcstream->end) + return 0; + return 1; +} + +static int +http_transfer_chunked_parse(struct http_transfer_chunked_istream *tcstream) +{ + int ret; + + /* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-21; + Section 4.1: + + chunked-body = *chunk + last-chunk + trailer-part + CRLF + + chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF + chunk-size = 1*HEXDIG + last-chunk = 1*("0") [ chunk-ext ] CRLF + + chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) + chunk-ext-name = token + chunk-ext-val = token / quoted-str-nf + chunk-data = 1*OCTET ; a sequence of chunk-size octets + trailer-part = *( header-field CRLF ) + + quoted-str-nf = DQUOTE *( qdtext-nf / quoted-pair ) DQUOTE + ; like quoted-string, but disallowing line folding + qdtext-nf = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + */ + + for (;;) { + switch (tcstream->state) { + case HTTP_CHUNKED_PARSE_STATE_INIT: + tcstream->chunk_size = 0; + tcstream->chunk_pos = 0; + tcstream->parsed_chars = 0; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_SIZE; + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_SIZE: + if ((ret=http_transfer_chunked_parse_size(tcstream)) <= 0) + return ret; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT; + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_EXT: + if (*tcstream->cur != ';') { + tcstream->state = HTTP_CHUNKED_PARSE_STATE_CR; + break; + } + /* chunk-ext */ + tcstream->cur++; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_NAME; + if (tcstream->cur >= tcstream->end) + return 0; + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_EXT_NAME: + /* chunk-ext-name = token */ + if ((ret=http_transfer_chunked_skip_token(tcstream)) <= 0) { + if (ret < 0) + tcstream->error = "Invalid chunked extension name"; + return ret; + } + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_EQ; + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_EXT_EQ: + if (*tcstream->cur != '=') { + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT; + break; + } + tcstream->cur++; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE; + if (tcstream->cur >= tcstream->end) + return 0; + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE: + /* chunk-ext-val = token / quoted-str-nf */ + if (*tcstream->cur != '"') { + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_TOKEN; + break; + } + tcstream->cur++; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING; + if (tcstream->cur >= tcstream->end) + return 0; + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING: + for (;;) { + if (*tcstream->cur == '"') { + tcstream->cur++; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT; + if (tcstream->cur >= tcstream->end) + return 0; + break; + } else if ((ret=http_transfer_chunked_skip_qdtext(tcstream)) <= 0) { + if (ret < 0) + tcstream->error = "Invalid chunked extension value"; + return ret; + } else if (*tcstream->cur == '\\') { + tcstream->cur++; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_ESCAPE; + if (tcstream->cur >= tcstream->end) + return 0; + break; + } else { + tcstream->error = t_strdup_printf( + "Invalid character %s in chunked extension value string", + _chr_sanitize(*tcstream->cur)); + return -1; + } + } + break; + case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_ESCAPE: + /* ( HTAB / SP / VCHAR / obs-text ) */ + if (!http_char_is_text(*tcstream->cur)) { + tcstream->error = t_strdup_printf( + "Escaped invalid character %s in chunked extension value string", + _chr_sanitize(*tcstream->cur)); + return -1; + } + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING; + if (tcstream->cur >= tcstream->end) + return 0; + break; + case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_TOKEN: + if ((ret=http_transfer_chunked_skip_token(tcstream)) <= 0) { + if (ret < 0) + tcstream->error = "Invalid chunked extension value"; + return ret; + } + tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT; + break; + case HTTP_CHUNKED_PARSE_STATE_CR: + tcstream->state = HTTP_CHUNKED_PARSE_STATE_LF; + if (*tcstream->cur == '\r') { + tcstream->cur++; + if (tcstream->cur >= tcstream->end) + return 0; + } + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_LF: + if (*tcstream->cur != '\n') { + tcstream->error = t_strdup_printf( + "Expected new line after chunk size, but found %s", + _chr_sanitize(*tcstream->cur)); + return -1; + } + tcstream->cur++; + if (tcstream->chunk_size > 0) + tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA; + else + tcstream->state = HTTP_CHUNKED_PARSE_STATE_TRAILER; + return 1; + case HTTP_CHUNKED_PARSE_STATE_DATA_READY: + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_DATA_CR: + tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA_LF; + if (*tcstream->cur == '\r') { + tcstream->cur++; + if (tcstream->cur >= tcstream->end) + return 0; + } + /* fall through */ + case HTTP_CHUNKED_PARSE_STATE_DATA_LF: + if (*tcstream->cur != '\n') { + tcstream->error = t_strdup_printf( + "Expected new line after chunk data, but found %s", + _chr_sanitize(*tcstream->cur)); + return -1; + } + tcstream->cur++; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_INIT; + break; + default: + i_unreached(); + } + } + + i_unreached(); + return -1; +} + +static int http_transfer_chunked_parse_next( + struct http_transfer_chunked_istream *tcstream) +{ + struct istream *input = tcstream->istream.parent; + size_t size; + int ret; + + while ((ret=i_stream_read_data + (input, &tcstream->begin, &size, 0)) > 0) { + tcstream->cur = tcstream->begin; + tcstream->end = tcstream->cur + size; + + if ((ret=http_transfer_chunked_parse(tcstream)) < 0) + return -1; + + i_stream_skip(input, tcstream->cur - tcstream->begin); + + if (ret > 0) { + if (tcstream->state == HTTP_CHUNKED_PARSE_STATE_DATA) + tcstream->chunk_v_offset = input->v_offset; + return ret; + } + } + + i_assert(ret != -2); + return ret; +} + +static ssize_t +http_transfer_chunked_istream_read_data( + struct http_transfer_chunked_istream *tcstream) +{ + struct istream_private *stream = &tcstream->istream; + const unsigned char *data; + size_t size, avail; + ssize_t ret = 0; + + if (tcstream->chunk_pos >= tcstream->chunk_size) { + tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA_READY; + return 0; + } + + // FIXME: is this even necessary? + i_stream_seek(stream->parent, tcstream->chunk_v_offset + tcstream->chunk_pos); + + /* read from parent if necessary */ + data = i_stream_get_data(stream->parent, &size); + if (size == 0) { + ret = i_stream_read(stream->parent); + if (ret <= 0 && (ret != -2 || stream->skip == 0)) { + if ( stream->parent->eof && stream->parent->stream_errno == 0 ) { + /* unexpected EOF */ + tcstream->error = "Unexpected end of payload"; + stream->istream.stream_errno = EIO; + } else { + /* parent stream error */ + tcstream->error = "Stream error"; + stream->istream.stream_errno = stream->parent->stream_errno; + } + return ret; + } + data = i_stream_get_data(stream->parent, &size); + i_assert(size != 0); + } + + size = size > (tcstream->chunk_size - tcstream->chunk_pos) ? + (tcstream->chunk_size - tcstream->chunk_pos) : size; + + /* Allocate buffer space */ + if (!i_stream_try_alloc(stream, size, &avail)) + return -2; + + /* Copy payload */ + size = size > avail ? avail : size; + memcpy(&stream->w_buffer[stream->pos], data, size); + + i_stream_skip(stream->parent, size); + + tcstream->chunk_pos += size; + if (tcstream->chunk_pos >= tcstream->chunk_size) + tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA_READY; + + if ( ret < 0 ) { + stream->pos = stream->pos+size; + return ret; + } + + ret = size; + stream->pos = stream->pos+size; + return ret; +} + +static int http_transfer_chunked_parse_trailer( + struct http_transfer_chunked_istream *tcstream) +{ + const char *field_name, *error; + const unsigned char *field_data; + size_t field_size; + int ret; + + if (tcstream->header_parser == NULL) { + tcstream->header_parser = http_header_parser_init(tcstream->istream.parent); + } + + while ((ret=http_header_parse_next_field(tcstream->header_parser, + &field_name, &field_data, &field_size, &error)) > 0) { + if (field_name == NULL) break; + } + + if (ret <= 0) { + if (ret < 0) { + tcstream->error = t_strdup_printf + ("Failed to parse chunked trailer: %s", error); + } + return ret; + } + return 1; +} + +static ssize_t +http_transfer_chunked_istream_read(struct istream_private *stream) +{ + struct http_transfer_chunked_istream *tcstream = + (struct http_transfer_chunked_istream *)stream; + ssize_t ret = 0; + + for (;;) { + switch (tcstream->state) { + case HTTP_CHUNKED_PARSE_STATE_FINISHED: + tcstream->istream.istream.eof = TRUE; + return -1; + case HTTP_CHUNKED_PARSE_STATE_DATA: + if ((ret=http_transfer_chunked_istream_read_data(tcstream)) != 0) + return ret; + if (tcstream->state != HTTP_CHUNKED_PARSE_STATE_DATA_READY) + return 0; + break; + case HTTP_CHUNKED_PARSE_STATE_TRAILER: + if ((ret=http_transfer_chunked_parse_trailer(tcstream)) <= 0) + return ret; + tcstream->state = HTTP_CHUNKED_PARSE_STATE_FINISHED; + tcstream->istream.istream.eof = TRUE; + return -1; + default: + if ((ret=http_transfer_chunked_parse_next(tcstream)) <= 0) + return ret; + } + } + + return -1; +} + +struct istream * +http_transfer_chunked_istream_create(struct istream *input) +{ + struct http_transfer_chunked_istream *tcstream; + + tcstream = i_new(struct http_transfer_chunked_istream, 1); + + tcstream->istream.max_buffer_size = + input->real_stream->max_buffer_size; + + tcstream->istream.read = http_transfer_chunked_istream_read; + + tcstream->istream.istream.readable_fd = FALSE; + tcstream->istream.istream.blocking = input->blocking; + tcstream->istream.istream.seekable = FALSE; + return i_stream_create(&tcstream->istream, input, i_stream_get_fd(input)); +} + + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/http-transfer.h Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,8 @@ +#ifndef HTTP_TRANSFER_H +#define HTTP_TRANSFER_H + +struct istream * + http_transfer_chunked_istream_create(struct istream *input); + +#endif +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/test-http-client.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,226 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "istream.h" +#include "write-full.h" +#include "http-response-parser.h" +#include "http-client.h" + +struct http_test_request { + struct io *io; + struct istream *payload; +}; + +static void payload_input(struct http_test_request *req) +{ + const unsigned char *data; + size_t size; + int ret; + + /* read payload */ + while ((ret=i_stream_read_data(req->payload, &data, &size, 0)) > 0) { + i_info("DEBUG: got data (size=%d)", (int)size); + //write_full(1, data, size); + i_stream_skip(req->payload, size); + } + + if (ret == 0) { + i_info("DEBUG: REQUEST: NEED MORE DATA"); + /* we will be called again for this request */ + } else { + if (req->payload->stream_errno != 0) + i_error("REQUEST PAYLOAD READ ERROR: %m"); + else + i_info("DEBUG: REQUEST: Finished"); + io_remove(&req->io); + i_stream_unref(&req->payload); + i_free(req); + } +} + +static void +got_request_response(const struct http_response *response, + struct http_test_request *req) +{ + if (response == NULL) { + /* request failed */ + i_free(req); + i_error("HTTP REQUEST FAILED"); + return; + } + + if (response->status / 100 != 2) { + i_error("HTTP Request failed: %s", response->reason); + i_free(req); + /* payload (if any) is skipped implicitly */ + return; + } + + i_info("DEBUG: REQUEST SUCCEEDED: %s", response->reason); + + if (response->payload == NULL) { + i_free(req); + return; + } + + i_info("DEBUG: REQUEST: Got payload"); + i_stream_ref(response->payload); + req->payload = response->payload; + req->io = io_add(i_stream_get_fd(response->payload), IO_READ, + payload_input, req); + payload_input(req); +} + +static const char *test_query1 = "action=fullsearch&context=180&value=sieve&titlesearch=Titles"; +static const char *test_query2 = "action=fullsearch&context=180&value=pigeonhole&titlesearch=Titles"; + +int main(void) +{ + struct http_client_settings http_set; + struct http_client *http_client; + struct http_client_request *http_req; + struct ioloop *ioloop; + struct http_test_request *test_req; + struct istream *post_payload; + + memset(&http_set, 0, sizeof(http_set)); + http_set.dns_client_socket_path = "/var/run/dovecot/dns-client"; + http_set.debug = TRUE; + http_set.ssl_ca_dir = "/etc/ssl/certs"; + http_set.max_idle_time_msecs = 5*1000; + http_set.max_parallel_connections = 4; + http_set.max_pipelined_requests = 4; + http_set.max_redirects = 1; + http_set.max_attempts = 1; + http_set.debug = TRUE; + http_set.rawlog_dir = "/tmp/http-test"; + + lib_init(); + + ioloop = io_loop_create(); + io_loop_set_running(ioloop); + + // JigSAW is useful for testing: http://jigsaw.w3.org/HTTP/ + // POST test server is useful for POST testing: http://posttestserver.com + + http_client = http_client_init(&http_set); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/download.html", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "jigsaw.w3.org", "/HTTP/300/301.html", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/frop.html", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "jigsaw.w3.org", "/HTTP/300/307.html", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/documentation.html", + got_request_response, test_req); + http_client_request_set_urgent(http_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "jigsaw.w3.org", "/HTTP/300/302.html", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "POST", "posttestserver.com", "/post.php", + got_request_response, test_req); + post_payload = i_stream_create_from_data + ((unsigned char *)test_query1, strlen(test_query1)); + http_client_request_set_payload(http_req, post_payload, TRUE); + i_stream_unref(&post_payload); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "POST", "posttestserver.com", "/post.php", + got_request_response, test_req); + post_payload = i_stream_create_from_data + ((unsigned char *)test_query2, strlen(test_query2)); + http_client_request_set_payload(http_req, post_payload, TRUE); + i_stream_unref(&post_payload); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/", + got_request_response, test_req); + http_client_request_set_port(http_req, 81); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "HEAD", "pigeonhole.dovecot.org", "/download.html", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/", + got_request_response, test_req); + http_client_request_set_ssl(http_req, TRUE); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/download.html", + got_request_response, test_req); + http_client_request_set_ssl(http_req, TRUE); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "pigeonhole.dovecot.org", "/documentation.html", + got_request_response, test_req); + http_client_request_set_ssl(http_req, TRUE); + http_client_request_submit(http_req); + http_client_request_abort(&http_req); + i_free(test_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "wiki2.dovecot.org", "/Pigeonhole", + got_request_response, test_req); + http_client_request_submit(http_req); + + test_req = i_new(struct http_test_request, 1); + http_req = http_client_request(http_client, + "GET", "jigsaw.w3.org", "/HTTP/ChunkedScript", + got_request_response, test_req); + http_client_request_submit(http_req); + + http_client_wait(http_client); + + http_client_deinit(&http_client); + + io_loop_destroy(&ioloop); + lib_deinit(); +}
--- a/src/lib-http/test-http-header-parser.c Fri Nov 23 23:49:39 2012 +0200 +++ b/src/lib-http/test-http-header-parser.c Sat Nov 24 00:30:14 2012 +0200 @@ -61,6 +61,10 @@ { NULL, NULL } }; +static struct http_header_parse_result valid_header_parse_result4[] = { + { NULL, NULL } +}; + static const struct http_header_parse_test valid_header_parse_tests[] = { { .header = "Date: Sat, 06 Oct 2012 16:01:44 GMT\r\n" @@ -103,6 +107,10 @@ "Content-Type: text/html; charset=iso-8859-1\r\n" "\r\n", .fields = valid_header_parse_result3 + },{ + .header = + "\r\n", + .fields = valid_header_parse_result4 } }; @@ -115,7 +123,7 @@ for (i = 0; i < valid_header_parse_test_count; i++) T_BEGIN { struct istream *input; struct http_header_parser *parser; - const char *header, *field_name; + const char *header, *field_name, *error; const unsigned char *field_data; size_t field_size; int ret; @@ -129,7 +137,7 @@ j = 0; while ((ret=http_header_parse_next_field - (parser, &field_name, &field_data, &field_size)) > 0) { + (parser, &field_name, &field_data, &field_size, &error)) > 0) { const struct http_header_parse_result *result; const char *field_value; @@ -191,11 +199,11 @@ for (i = 0; i < invalid_header_parse_test_count; i++) T_BEGIN { struct istream *input; struct http_header_parser *parser; - const char *header, *field_name; + const char *header, *field_name, *error; const unsigned char *field_data; size_t field_size; int ret; - + header = invalid_header_parse_tests[i]; input = i_stream_create_from_data(header, strlen(header)); parser = http_header_parser_init(input); @@ -203,7 +211,7 @@ test_begin(t_strdup_printf("http header invalid [%d]", i)); while ((ret=http_header_parse_next_field - (parser, &field_name, &field_data, &field_size)) > 0) { + (parser, &field_name, &field_data, &field_size, &error)) > 0) { if (field_name == NULL) break; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/test-http-response-parser.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,206 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "test-lib.h" +#include "buffer.h" +#include "str.h" +#include "str-sanitize.h" +#include "istream.h" +#include "ostream.h" +#include "test-common.h" +#include "http-response-parser.h" + +#include <time.h> + +struct http_response_parse_test { + const char *response; + unsigned char version_major; + unsigned char version_minor; + unsigned int status; + uoff_t content_length; + const char *payload; +}; + +/* Valid header tests */ + +static const struct http_response_parse_test +valid_response_parse_tests[] = { + { .response = + "HTTP/1.1 200 OK\r\n" + "Date: Sun, 07 Oct 2012 13:02:27 GMT\r\n" + "Server: Apache/2.2.16 (Debian)\r\n" + "Last-Modified: Tue, 18 Sep 2012 19:31:41 GMT\r\n" + "Etag: \"2a8400c-10751f-4c9fef0858140\"\r\n" + "Accept-Ranges: bytes\r\n" + "Content-Length: 33\r\n" + "Keep-Alive: timeout=15, max=100\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "This is a piece of stupid text.\r\n", + .status = 200, + .payload = "This is a piece of stupid text.\r\n" + },{ + .response = + "HTTP/1.1 200 OK\r\n" + "Date: Sun, 07 Oct 2012 13:02:27 GMT\r\n" + "Server: Apache/2.2.16 (Debian)\r\n" + "Last-Modified: Tue, 18 Sep 2012 19:31:41 GMT\r\n" + "Etag: \"2a8400c-10751f-4c9fef0858140\"\r\n" + "Accept-Ranges: bytes\r\n" + "Content-Length: 33\r\n" + "Keep-Alive: timeout=15, max=100\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "This is a piece of stupid text.\r\n" + "HTTP/1.1 200 OK\r\n" + "Date: Sun, 07 Oct 2012 13:02:27 GMT\r\n" + "Server: Apache/2.2.16 (Debian)\r\n" + "Last-Modified: Tue, 18 Sep 2012 19:31:41 GMT\r\n" + "Etag: \"2a8400c-10751f-4c9fef0858140\"\r\n" + "Accept-Ranges: bytes\r\n" + "Content-Length: 43\r\n" + "Keep-Alive: timeout=15, max=100\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "This is a piece of even more stupid text.\r\n", + .status = 200, + .payload = "This is a piece of even more stupid text.\r\n" + },{ + .response = + "HTTP/1.1 401 Authorization Required\r\n" + "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n" + "Server: Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14\r\n" + "WWW-Authenticate: Basic realm=\"Munin\"\r\n" + "Vary: Accept-Encoding\r\n" + "Content-Encoding: gzip\r\n" + "Content-Length: 5\r\n" + "Keep-Alive: timeout=15, max=99\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/html; charset=iso-8859-1\r\n" + "\r\n" + "Frop!", + .status = 401, + .payload = "Frop!" + } +}; + +unsigned int valid_response_parse_test_count = + N_ELEMENTS(valid_response_parse_tests); + +static void test_http_response_parse_valid(void) +{ + unsigned int i; + buffer_t *payload_buffer = buffer_create_dynamic(default_pool, 1024); + + for (i = 0; i < valid_response_parse_test_count; i++) T_BEGIN { + struct istream *input; + struct ostream *output; + const struct http_response_parse_test *test; + struct http_response_parser *parser; + struct http_response *response; + const char *response_text, *payload, *error; + int ret; + + test = &valid_response_parse_tests[i]; + response_text = test->response; + input = i_stream_create_from_data(response_text, strlen(response_text)); + parser = http_response_parser_init(input); + + test_begin(t_strdup_printf("http response valid [%d]", i)); + + payload = NULL; + while ((ret=http_response_parse_next(parser, FALSE, &response, &error)) > 0) { + if (response->payload != NULL) { + buffer_set_used_size(payload_buffer, 0); + output = o_stream_create_buffer(payload_buffer); + test_out("payload receive", + o_stream_send_istream(output, response->payload)); + o_stream_destroy(&output); + payload = str_c(payload_buffer); + } else { + payload = NULL; + } + } + + test_out("parse success", ret == 0); + + if (ret == 0) { + /* verify last response only */ + test_out(t_strdup_printf("response->status = %d",test->status), + response->status == test->status); + if (payload == NULL || test->payload == NULL) { + test_out(t_strdup_printf("response->payload = %s", + str_sanitize(payload, 80)), + payload == test->payload); + } else { + test_out(t_strdup_printf("response->payload = %s", + str_sanitize(payload, 80)), + strcmp(payload, test->payload) == 0); + } + } + test_end(); + http_response_parser_deinit(&parser); + } T_END; + + buffer_free(&payload_buffer); +} + +static const char *invalid_response_parse_tests[] = { + "XMPP/1.0 302 Found\r\n" + "Location: http://www.example.nl/\r\n" + "Cache-Control: private\r\n", + "HTTP/1.1 302 Found\r\n" + "Location: http://www.example.nl/\r\n" + "Cache-Control: private\r\n", + "HTTP/1.1 ABC Found\r\n" + "Location: http://www.example.nl/\r\n" + "Cache-Control: private\r\n", + "HTTP/1.1 302 \177\r\n" + "Location: http://www.example.nl/\r\n" + "Cache-Control: private\r\n", + "HTTP/1.1 302 Found\n\r" + "Location: http://www.example.nl/\n\r" + "Cache-Control: private\n\r" +}; + +unsigned int invalid_response_parse_test_count = + N_ELEMENTS(invalid_response_parse_tests); + +static void test_http_response_parse_invalid(void) +{ + unsigned int i; + + for (i = 0; i < invalid_response_parse_test_count; i++) T_BEGIN { + struct istream *input; + const char *test; + struct http_response_parser *parser; + struct http_response *response; + const char *response_text, *error; + int ret; + + test = invalid_response_parse_tests[i]; + response_text = test; + input = i_stream_create_from_data(response_text, strlen(response_text)); + parser = http_response_parser_init(input); + + test_begin(t_strdup_printf("http response invalid [%d]", i)); + + while ((ret=http_response_parse_next(parser, FALSE, &response, &error)) > 0); + + test_out("parse failure", ret < 0); + test_end(); + http_response_parser_deinit(&parser); + } T_END; +} + +int main(void) +{ + static void (*test_functions[])(void) = { + test_http_response_parse_valid, + test_http_response_parse_invalid, + NULL + }; + return test_run(test_functions); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/test-http-responses.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,39 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "test-lib.h" +#include "buffer.h" +#include "str.h" +#include "str-sanitize.h" +#include "istream.h" +#include "ostream.h" +#include "test-common.h" +#include "http-response-parser.h" + +#include <stdio.h> + +// FIXME: debug tool; can be removed + +int main(int argc, char **argv) +{ + struct istream *input; + struct http_response_parser *parser; + struct http_response *response; + const char *payload, *error = NULL; + int ret; + + if (argc < 2) + return 1; + + input = i_stream_create_file(argv[1], 32); + parser = http_response_parser_init(input); + + payload = NULL; + while ((ret=http_response_parse_next(parser, FALSE, &response, &error)) > 0) { + printf("RESPONSE: %u %s\n", response->status, response->reason); + } + + printf("RET: %d %s\n", ret, error); + + http_response_parser_deinit(&parser); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-http/test-http-transfer.c Sat Nov 24 00:30:14 2012 +0200 @@ -0,0 +1,126 @@ +/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */ + +#include "test-lib.h" +#include "buffer.h" +#include "str.h" +#include "strfuncs.h" +#include "str-sanitize.h" +#include "istream.h" +#include "ostream.h" +#include "test-common.h" +#include "http-transfer.h" + +#include <time.h> + +struct http_transfer_chunked_test { + const char *in; + const char *out; +}; + +/* Valid transfer_chunked tests */ +struct http_transfer_chunked_test valid_transfer_chunked_tests[] = { + { .in = "1E\r\n" + "This is a simple test payload." + "\r\n" + "0\r\n" + "\r\n", + .out = + "This is a simple test payload." + }, + { .in = "20\r\n" + "This is a longer test payload..." + "\r\n" + "23\r\n" + "...spread over two separate chunks." + "\r\n" + "0\r\n" + "\r\n", + .out = + "This is a longer test payload..." + "...spread over two separate chunks." + }, + { .in = "26\r\n" + "This is an even longer test payload..." + "\r\n" + "27\r\n" + "...spread over three separate chunks..." + "\r\n" + "1F\r\n" + "...and also includes a trailer." + "\r\n" + "0\r\n" + "Checksum: adgfef3fdaf3daf3dfaf3ff3fdag\r\n" + "X-Dovecot: Whatever\r\n" + "\r\n", + .out = + "This is an even longer test payload..." + "...spread over three separate chunks..." + "...and also includes a trailer." + }, + { .in = "26\n" + "This is an even longer test payload..." + "\n" + "27\n" + "...spread over three separate chunks..." + "\n" + "1F\n" + "...and also includes a trailer." + "\n" + "0\n" + "Checksum: adgfef3fdaf3daf3dfaf3ff3fdag\n" + "X-Dovecot: Whatever\n" + "\n", + .out = + "This is an even longer test payload..." + "...spread over three separate chunks..." + "...and also includes a trailer." + } +}; + +unsigned int valid_transfer_chunked_test_count = + N_ELEMENTS(valid_transfer_chunked_tests); + +static void test_http_transfer_chunked_valid(void) +{ + struct istream *input, *chunked; + struct ostream *output; + buffer_t *payload_buffer; + unsigned int i; + + payload_buffer = buffer_create_dynamic(default_pool, 1024); + + for (i = 0; i < valid_transfer_chunked_test_count; i++) T_BEGIN { + const char *in, *out, *stream_out; + + in = valid_transfer_chunked_tests[i].in; + out = valid_transfer_chunked_tests[i].out; + + test_begin(t_strdup_printf("http transfer_chunked valid [%d]", i)); + + input = i_stream_create_from_data(in, strlen(in)); + chunked = http_transfer_chunked_istream_create(input); + + buffer_set_used_size(payload_buffer, 0); + output = o_stream_create_buffer(payload_buffer); + test_out("payload read", + o_stream_send_istream(output, chunked)); + o_stream_destroy(&output); + stream_out = str_c(payload_buffer); + + test_out(t_strdup_printf("response->payload = %s", + str_sanitize(stream_out, 80)), + strcmp(stream_out, out) == 0); + test_end(); + } T_END; + + buffer_free(&payload_buffer); +} + +int main(void) +{ + static void (*test_functions[])(void) = { + test_http_transfer_chunked_valid, + NULL + }; + return test_run(test_functions); +}