summaryrefslogtreecommitdiffstats
path: root/src/lib-http/test-http-client-errors.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/lib-http/test-http-client-errors.c3944
1 files changed, 3944 insertions, 0 deletions
diff --git a/src/lib-http/test-http-client-errors.c b/src/lib-http/test-http-client-errors.c
new file mode 100644
index 0000000..e127041
--- /dev/null
+++ b/src/lib-http/test-http-client-errors.c
@@ -0,0 +1,3944 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "http-url.h"
+#include "http-request.h"
+#include "http-client.h"
+
+#include <unistd.h>
+#include <sys/signal.h>
+
+#define CLIENT_PROGRESS_TIMEOUT 10
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ struct connection conn;
+ void *context;
+
+ pool_t pool;
+ bool version_sent:1;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool
+(*test_client_init_t)(const struct http_client_settings *client_set);
+typedef void (*test_dns_init_t)(void);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static struct connection_list *server_conn_list;
+static size_t server_read_max = 0;
+static unsigned int server_index;
+static int (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+static void (*test_server_input)(struct server_connection *conn);
+
+/* client */
+static struct timeout *to_client_progress = NULL;
+static struct http_client *http_client = NULL;
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_run(unsigned int index);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_defaults(struct http_client_settings *http_set);
+static void test_client_deinit(void);
+
+/* test*/
+static void
+test_run_client_server(const struct http_client_settings *client_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count,
+ test_dns_init_t dns_test) ATTR_NULL(3);
+
+/*
+ * Utility
+ */
+
+static void
+test_client_assert_response(const struct http_response *resp,
+ bool condition)
+{
+ const char *reason = (resp->reason != NULL ? resp->reason : "<NULL>");
+
+ test_assert(resp->reason != NULL && *resp->reason != '\0');
+
+ if (!condition)
+ i_error("BAD RESPONSE: %u %s", resp->status, reason);
+ else if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+}
+
+/*
+ * Unconfigured SSL
+ */
+
+/* client */
+
+struct _unconfigured_ssl {
+ unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_response(const struct http_response *resp,
+ struct _unconfigured_ssl *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_unconfigured_ssl(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _unconfigured_ssl *ctx;
+
+ ctx = i_new(struct _unconfigured_ssl, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl.txt",
+ test_client_unconfigured_ssl_response, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl2.txt",
+ test_client_unconfigured_ssl_response, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("unconfigured ssl");
+ test_run_client_server(&http_client_set,
+ test_client_unconfigured_ssl, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Unconfigured SSL abort
+ */
+
+/* client */
+
+struct _unconfigured_ssl_abort {
+ unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_abort_response1(
+ const struct http_response *resp,
+ struct _unconfigured_ssl_abort *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ test_out_quiet("inappropriate callback", FALSE);
+}
+
+static void
+test_client_unconfigured_ssl_abort_response2(
+ const struct http_response *resp, struct _unconfigured_ssl_abort *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unconfigured_ssl_abort(
+ const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _unconfigured_ssl_abort *ctx;
+
+ ctx = i_new(struct _unconfigured_ssl_abort, 1);
+ ctx->count = 1;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl.txt",
+ test_client_unconfigured_ssl_abort_response1, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+ http_client_request_abort(&hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl2.txt",
+ test_client_unconfigured_ssl_abort_response2, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl_abort(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("unconfigured ssl abort");
+ test_run_client_server(&http_client_set,
+ test_client_unconfigured_ssl_abort,
+ NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Invalid URL
+ */
+
+/* client */
+
+struct _invalid_url {
+ unsigned int count;
+};
+
+static void
+test_client_invalid_url_response(const struct http_response *resp,
+ struct _invalid_url *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_INVALID_URL);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_invalid_url(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _invalid_url *ctx;
+
+ ctx = i_new(struct _invalid_url, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request_url_str(
+ http_client, "GET", "imap://example.com/INBOX",
+ test_client_invalid_url_response, ctx);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request_url_str(
+ http_client, "GET", "http:/www.example.com",
+ test_client_invalid_url_response, ctx);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_invalid_url(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("invalid url");
+ test_run_client_server(&http_client_set,
+ test_client_invalid_url, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Host lookup failed
+ */
+
+/* client */
+
+struct _host_lookup_failed {
+ unsigned int count;
+};
+
+static void
+test_client_host_lookup_failed_response(const struct http_response *resp,
+ struct _host_lookup_failed *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_host_lookup_failed(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _host_lookup_failed *ctx;
+
+ ctx = i_new(struct _host_lookup_failed, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "host.in-addr.arpa",
+ "/host-lookup-failed.txt",
+ test_client_host_lookup_failed_response, ctx);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "host.in-addr.arpa",
+ "/host-lookup-failed2.txt",
+ test_client_host_lookup_failed_response, ctx);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_host_lookup_failed(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("host lookup failed");
+ test_run_client_server(&http_client_set,
+ test_client_host_lookup_failed, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void
+test_server_connection_refused(unsigned int index ATTR_UNUSED)
+{
+ i_close_fd(&fd_listen);
+
+ test_subprocess_notify_signal_send_parent(SIGUSR1);
+}
+
+/* client */
+
+struct _connection_refused {
+ unsigned int count;
+ struct timeout *to;
+};
+
+static void
+test_client_connection_refused_response(const struct http_response *resp,
+ struct _connection_refused *ctx)
+{
+ test_assert(ctx->to == NULL);
+ timeout_remove(&ctx->to);
+
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void
+test_client_connection_refused_timeout(struct _connection_refused *ctx)
+{
+ if (debug)
+ i_debug("TIMEOUT (ok)");
+ timeout_remove(&ctx->to);
+}
+
+static bool
+test_client_connection_refused(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _connection_refused *ctx;
+
+ /* wait for the server side to close the socket */
+ test_subprocess_notify_signal_wait(SIGUSR1, 10000);
+
+ ctx = i_new(struct _connection_refused, 1);
+ ctx->count = 2;
+
+ if (client_set->max_connect_attempts > 0) {
+ ctx->to = timeout_add_short(250,
+ test_client_connection_refused_timeout, ctx);
+ }
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused.txt",
+ test_client_connection_refused_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused2.txt",
+ test_client_connection_refused_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("connection refused");
+ test_subprocess_notify_signal_reset(SIGUSR1);
+ test_run_client_server(&http_client_set,
+ test_client_connection_refused,
+ test_server_connection_refused, 1, NULL);
+ test_end();
+
+ http_client_set.max_connect_attempts = 3;
+
+ test_begin("connection refused backoff");
+ test_subprocess_notify_signal_reset(SIGUSR1);
+ test_run_client_server(&http_client_set,
+ test_client_connection_refused,
+ test_server_connection_refused, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost prematurely
+ */
+
+/* server */
+
+static void
+test_server_connection_lost_prematurely_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_lost_prematurely(unsigned int index)
+{
+ test_server_input = test_server_connection_lost_prematurely_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_prematurely {
+ unsigned int count;
+ struct timeout *to;
+};
+
+static void
+test_client_connection_lost_prematurely_response(
+ const struct http_response *resp,
+ struct _connection_lost_prematurely *ctx)
+{
+ test_assert(ctx->to == NULL);
+ timeout_remove(&ctx->to);
+
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void
+test_client_connection_lost_prematurely_timeout(
+ struct _connection_lost_prematurely *ctx)
+{
+ if (debug)
+ i_debug("TIMEOUT (ok)");
+ timeout_remove(&ctx->to);
+}
+
+static bool
+test_client_connection_lost_prematurely(
+ const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _connection_lost_prematurely *ctx;
+
+ ctx = i_new(struct _connection_lost_prematurely, 1);
+ ctx->count = 2;
+
+ ctx->to = timeout_add_short(
+ 250, test_client_connection_lost_prematurely_timeout, ctx);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused-retry.txt",
+ test_client_connection_lost_prematurely_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused-retry2.txt",
+ test_client_connection_lost_prematurely_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_prematurely(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_connect_attempts = 3;
+ http_client_set.max_attempts = 3;
+
+ test_begin("connection lost prematurely");
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost_prematurely,
+ test_server_connection_lost_prematurely, 1,
+ NULL);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* client */
+
+struct _connection_timed_out {
+ unsigned int count;
+};
+
+static void
+test_client_connection_timed_out_response(const struct http_response *resp,
+ struct _connection_timed_out *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_timed_out(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _connection_timed_out *ctx;
+
+ ctx = i_new(struct _connection_timed_out, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "192.168.0.0", "/connection-timed-out.txt",
+ test_client_connection_timed_out_response, ctx);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "192.168.0.0", "/connection-timed-out2.txt",
+ test_client_connection_timed_out_response, ctx);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.connect_timeout_msecs = 1000;
+ http_client_set.max_attempts = 1;
+
+ test_begin("connection timed out");
+ test_run_client_server(&http_client_set,
+ test_client_connection_timed_out, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Invalid redirect
+ */
+
+/* server */
+
+/* -> not accepted */
+
+static void test_invalid_redirect_input1(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: http://localhost:4444\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_invalid_redirect1(unsigned int index)
+{
+ test_server_input = test_invalid_redirect_input1;
+ test_server_run(index);
+}
+
+/* -> bad location */
+
+static void test_invalid_redirect_input2(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: unix:/var/run/dovecot/auth-master\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_invalid_redirect2(unsigned int index)
+{
+ test_server_input = test_invalid_redirect_input2;
+ test_server_run(index);
+}
+
+/* -> too many */
+
+static void test_invalid_redirect_input3(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: http://%s:%u/friep.txt\r\n"
+ "\r\n",
+ net_ip2addr(&bind_ip), bind_ports[server_index+1]);
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_invalid_redirect3(unsigned int index)
+{
+ test_server_input = test_invalid_redirect_input3;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_invalid_redirect_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_invalid_redirect(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/invalid-redirect.txt",
+ test_client_invalid_redirect_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_invalid_redirect(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("invalid redirect: not accepted");
+ http_client_set.max_redirects = 0;
+ test_run_client_server(&http_client_set,
+ test_client_invalid_redirect,
+ test_server_invalid_redirect1, 1, NULL);
+ test_end();
+
+ test_begin("invalid redirect: bad location");
+ http_client_set.max_redirects = 1;
+ test_run_client_server(&http_client_set,
+ test_client_invalid_redirect,
+ test_server_invalid_redirect2, 1, NULL);
+ test_end();
+
+ test_begin("invalid redirect: too many");
+ http_client_set.max_redirects = 1;
+ test_run_client_server(&http_client_set,
+ test_client_invalid_redirect,
+ test_server_invalid_redirect3, 3, NULL);
+ test_end();
+}
+
+/*
+ * Unseekable redirect
+ */
+
+/* server */
+
+static void test_unseekable_redirect_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: http://%s:%u/frml.txt\r\n"
+ "\r\n",
+ net_ip2addr(&bind_ip), bind_ports[server_index+1]);
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_unseekable_redirect(unsigned int index)
+{
+ test_server_input = test_unseekable_redirect_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_unseekable_redirect_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_ABORTED);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unseekable_redirect(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data("FROP", 4);
+ input->seekable = FALSE;
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/unseekable-redirect.txt",
+ test_client_unseekable_redirect_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_unseekable_redirect(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_redirects = 1;
+
+ test_begin("unseekable redirect");
+ test_run_client_server(&http_client_set,
+ test_client_unseekable_redirect,
+ test_server_unseekable_redirect, 2, NULL);
+ test_end();
+}
+
+/*
+ * Unseekable retry
+ */
+
+/* server */
+
+static void test_unseekable_retry_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_server_unseekable_retry(unsigned int index)
+{
+ test_server_input = test_unseekable_retry_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_unseekable_retry_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_ABORTED);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unseekable_retry(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data("FROP", 4);
+ input->seekable = FALSE;
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/unseekable-retry.txt",
+ test_client_unseekable_retry_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_unseekable_retry(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_attempts = 3;
+
+ test_begin("unseekable retry");
+ test_run_client_server(&http_client_set,
+ test_client_unseekable_retry,
+ test_server_unseekable_retry, 2, NULL);
+ test_end();
+}
+
+/*
+ * Broken payload
+ */
+
+/* server */
+
+static void test_broken_payload_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Everything is OK\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_broken_payload(unsigned int index)
+{
+ test_server_input = test_broken_payload_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_broken_payload_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_BROKEN_PAYLOAD);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_broken_payload(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ test_expect_errors(1);
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_error_str(EIO, "Moehahahaha!!");
+ i_stream_set_name(input, "PURE EVIL");
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/broken-payload.txt",
+ test_client_broken_payload_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_broken_payload(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("broken payload");
+ test_run_client_server(&http_client_set,
+ test_client_broken_payload,
+ test_server_broken_payload, 1, NULL);
+ test_end();
+}
+
+/*
+ * Retry payload
+ */
+
+/* server */
+
+struct _retry_payload_sctx {
+ bool eoh;
+};
+
+static int test_retry_payload_init(struct server_connection *conn)
+{
+ struct _retry_payload_sctx *ctx;
+
+ ctx = p_new(conn->pool, struct _retry_payload_sctx, 1);
+ conn->context = ctx;
+ return 0;
+}
+
+static void test_retry_payload_input(struct server_connection *conn)
+{
+ struct _retry_payload_sctx *ctx = conn->context;
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (*line == '\0') {
+ ctx->eoh = TRUE;
+ continue;
+ }
+ if (ctx->eoh)
+ break;
+ }
+
+ if (conn->conn.input->stream_errno != 0) {
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ i_fatal("server: Client stream ended prematurely");
+ return;
+ }
+
+ i_assert(ctx->eoh);
+
+ if (strcmp(line, "This is the payload we expect.") == 0) {
+ if (debug)
+ i_debug("Expected payload received");
+ o_stream_nsend_str(conn->conn.output,
+ "HTTP/1.1 500 Oh no!\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 17\r\n"
+ "\r\n"
+ "Expected result\r\n");
+ } else {
+ i_error("Unexpected payload received: `%s'",
+ str_sanitize(line, 128));
+ o_stream_nsend_str(conn->conn.output,
+ "HTTP/1.1 501 Oh no!\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 19\r\n"
+ "\r\n"
+ "Unexpected result\r\n");
+ }
+ server_connection_deinit(&conn);
+}
+
+static void test_server_retry_payload(unsigned int index)
+{
+ test_server_init = test_retry_payload_init;
+ test_server_input = test_retry_payload_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _retry_payload_ctx {
+ unsigned int count;
+};
+
+struct _retry_payload_request_ctx {
+ struct _retry_payload_ctx *ctx;
+ struct http_client_request *req;
+};
+
+static void
+test_client_retry_payload_response(const struct http_response *resp,
+ struct _retry_payload_request_ctx *rctx)
+{
+ struct _retry_payload_ctx *ctx = rctx->ctx;
+
+ test_client_assert_response(resp, resp->status == 500);
+
+ if (http_client_request_try_retry(rctx->req)) {
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+ i_free(rctx);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_retry_payload(const struct http_client_settings *client_set)
+{
+ static const char payload[] = "This is the payload we expect.\r\n";
+ struct _retry_payload_ctx *ctx;
+ struct _retry_payload_request_ctx *rctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _retry_payload_ctx, 1);
+ ctx->count = 1;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ rctx = i_new(struct _retry_payload_request_ctx, 1);
+ rctx->ctx = ctx;
+
+ rctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/retry-payload.txt",
+ test_client_retry_payload_response, rctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_retry_payload(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_attempts = 2;
+
+ server_read_max = 0;
+
+ test_begin("retry payload");
+ test_run_client_server(&http_client_set,
+ test_client_retry_payload,
+ test_server_retry_payload, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost
+ */
+
+/* server */
+
+static void test_connection_lost_input(struct server_connection *conn)
+{
+ ssize_t ret;
+
+ if (server_read_max == 0) {
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ i_stream_set_max_buffer_size(conn->conn.input, server_read_max);
+ ret = i_stream_read(conn->conn.input);
+ if (ret == -2) {
+ server_connection_deinit(&conn);
+ return;
+ }
+ if (ret < 0) {
+ i_assert(conn->conn.input->eof);
+ if (conn->conn.input->stream_errno == 0)
+ i_fatal("server: Client stream ended prematurely");
+ else
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+}
+
+static void test_server_connection_lost(unsigned int index)
+{
+ test_server_input = test_connection_lost_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_ctx {
+ unsigned int count;
+};
+
+struct _connection_lost_request_ctx {
+ struct _connection_lost_ctx *ctx;
+ struct http_client_request *req;
+};
+
+static void
+test_client_connection_lost_response(const struct http_response *resp,
+ struct _connection_lost_request_ctx *rctx)
+{
+ struct _connection_lost_ctx *ctx = rctx->ctx;
+
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+
+ if (http_client_request_try_retry(rctx->req)) {
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+ i_free(rctx);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost(const struct http_client_settings *client_set)
+{
+ static const char payload[] =
+ "This is a useless payload that only serves as a means to give "
+ "the server the opportunity to close the connection before the "
+ "payload is finished.";
+ struct _connection_lost_ctx *ctx;
+ struct _connection_lost_request_ctx *rctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _connection_lost_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ rctx = i_new(struct _connection_lost_request_ctx, 1);
+ rctx->ctx = ctx;
+
+ rctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost.txt",
+ test_client_connection_lost_response, rctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ rctx = i_new(struct _connection_lost_request_ctx, 1);
+ rctx->ctx = ctx;
+
+ rctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost2.txt",
+ test_client_connection_lost_response, rctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ server_read_max = 0;
+
+ test_begin("connection lost: one attempt");
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+
+ test_begin("connection lost: two attempts");
+ http_client_set.max_attempts = 2;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+
+ test_begin("connection lost: three attempts");
+ http_client_set.max_attempts = 3;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+
+ test_begin("connection lost: manual retry");
+ http_client_set.max_attempts = 3;
+ http_client_set.no_auto_retry = TRUE;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost after 100-continue
+ */
+
+/* server */
+
+static void test_connection_lost_100_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 100 Continue\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_lost_100(unsigned int index)
+{
+ test_server_input = test_connection_lost_100_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_100_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_connection_lost_100_response(const struct http_response *resp,
+ struct _connection_lost_100_ctx *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost_100(
+ const struct http_client_settings *client_set)
+{
+ static const char payload[] =
+ "This is a useless payload that only serves as a means to give "
+ "the server the opportunity to close the connection before the "
+ "payload is finished.";
+ struct _connection_lost_100_ctx *ctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _connection_lost_100_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost.txt",
+ test_client_connection_lost_100_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost2.txt",
+ test_client_connection_lost_100_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_100(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ server_read_max = 0;
+
+ test_begin("connection lost after 100-continue");
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost_100,
+ test_server_connection_lost_100, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost in sub-ioloop
+ */
+
+/* server */
+
+static void
+test_connection_lost_sub_ioloop_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_lost_sub_ioloop(unsigned int index)
+{
+ test_server_input = test_connection_lost_sub_ioloop_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_sub_ioloop_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_connection_lost_sub_ioloop_response2(
+ const struct http_response *resp, struct ioloop *sub_ioloop)
+{
+ test_client_assert_response(
+ resp,
+ (resp->status == 200 ||
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST));
+
+ io_loop_stop(sub_ioloop);
+}
+
+static void
+test_client_connection_lost_sub_ioloop_response(
+ const struct http_response *resp,
+ struct _connection_lost_sub_ioloop_ctx *ctx)
+{
+ struct http_client_request *hreq;
+ struct ioloop *sub_ioloop;
+
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ test_assert(resp->status == 200 ||
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+ test_assert(resp->reason != NULL && *resp->reason != '\0');
+
+ sub_ioloop = io_loop_create();
+ http_client_switch_ioloop(http_client);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost-sub-ioloop3.txt",
+ test_client_connection_lost_sub_ioloop_response2, sub_ioloop);
+ http_client_request_set_port(hreq, bind_ports[1]);
+ http_client_request_submit(hreq);
+
+ io_loop_run(sub_ioloop);
+ io_loop_set_current(ioloop);
+ http_client_switch_ioloop(http_client);
+ io_loop_set_current(sub_ioloop);
+ io_loop_destroy(&sub_ioloop);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost_sub_ioloop(
+ const struct http_client_settings *client_set)
+{
+ static const char payload[] =
+ "This is a useless payload that only serves as a means to give "
+ "the server the opportunity to close the connection before the "
+ "payload is finished.";
+ struct _connection_lost_sub_ioloop_ctx *ctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _connection_lost_sub_ioloop_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost-sub-ioloop.txt",
+ test_client_connection_lost_sub_ioloop_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost-sub-ioloop2.txt",
+ test_client_connection_lost_sub_ioloop_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_sub_ioloop(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ server_read_max = 0;
+
+ test_begin("connection lost while running sub-ioloop");
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost_sub_ioloop,
+ test_server_connection_lost_sub_ioloop, 2, NULL);
+ test_end();
+}
+
+/*
+ * Early success
+ */
+
+/* server */
+
+static void test_early_success_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Everything is OK\r\n";
+
+ i_sleep_msecs(200);
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_early_success(unsigned int index)
+{
+ test_server_input = test_early_success_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _early_success_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_early_success_response(const struct http_response *resp,
+ struct _early_success_ctx *ctx)
+{
+ if (ctx->count == 2) {
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE);
+ } else {
+ test_client_assert_response(resp, resp->status == 200);
+ }
+
+ if (--ctx->count == 0) {
+ io_loop_stop(ioloop);
+ i_free(ctx);
+ }
+}
+
+static bool
+test_client_early_success(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _early_success_ctx *ctx;
+ string_t *payload;
+ unsigned int i;
+
+ ctx = i_new(struct _early_success_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/early-success.txt",
+ test_client_early_success_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+
+ T_BEGIN {
+ struct istream_chain *chain;
+ struct istream *input, *chain_input;
+
+ payload = t_str_new(64*3000);
+ for (i = 0; i < 3000; i++) {
+ str_append(payload,
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n"
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
+ }
+
+ chain_input = i_stream_create_chain(&chain, IO_BLOCK_SIZE);
+
+ input = i_stream_create_copy_from_data(str_data(payload),
+ str_len(payload));
+ i_stream_chain_append(chain, input);
+ i_stream_unref(&input);
+
+ http_client_request_set_payload(hreq, chain_input, FALSE);
+ i_stream_unref(&chain_input);
+ } T_END;
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/early-success2.txt",
+ test_client_early_success_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_early_success(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.socket_send_buffer_size = 4096;
+
+ test_begin("early succes");
+ test_run_client_server(&http_client_set,
+ test_client_early_success,
+ test_server_early_success, 1, NULL);
+ test_end();
+}
+
+/*
+ * Bad response
+ */
+
+/* server */
+
+static void test_bad_response_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 666 Really bad response\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_bad_response(unsigned int index)
+{
+ test_server_input = test_bad_response_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _bad_response_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_bad_response_response(const struct http_response *resp,
+ struct _bad_response_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_bad_response(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _bad_response_ctx *ctx;
+
+ ctx = i_new(struct _bad_response_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/bad-response.txt",
+ test_client_bad_response_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/bad-response2.txt",
+ test_client_bad_response_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_bad_response(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("bad response");
+ test_run_client_server(&http_client_set,
+ test_client_bad_response,
+ test_server_bad_response, 1, NULL);
+ test_end();
+}
+
+/*
+ * Request timed out
+ */
+
+/* server */
+
+static void
+test_request_timed_out_input(struct server_connection *conn ATTR_UNUSED)
+{
+ /* do nothing */
+}
+
+static void test_server_request_timed_out(unsigned int index)
+{
+ test_server_input = test_request_timed_out_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _request_timed_out1_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_request_timed_out1_response(const struct http_response *resp,
+ struct _request_timed_out1_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_request_timed_out1(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _request_timed_out1_ctx *ctx;
+
+ ctx = i_new(struct _request_timed_out1_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out1-1.txt",
+ test_client_request_timed_out1_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out1-2.txt",
+ test_client_request_timed_out1_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+struct _request_timed_out2_ctx {
+ struct timeout *to;
+ unsigned int count;
+ unsigned int max_parallel_connections;
+};
+
+static void
+test_client_request_timed_out2_timeout(struct _request_timed_out2_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+ i_debug("TIMEOUT");
+}
+
+static void
+test_client_request_timed_out2_response(const struct http_response *resp,
+ struct _request_timed_out2_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT);
+ test_assert(ctx->to != NULL);
+
+ if (--ctx->count > 0) {
+ if (ctx->to != NULL && ctx->max_parallel_connections <= 1)
+ timeout_reset(ctx->to);
+ } else {
+ timeout_remove(&ctx->to);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_request_timed_out2(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _request_timed_out2_ctx *ctx;
+
+ ctx = i_new(struct _request_timed_out2_ctx, 1);
+ ctx->count = 2;
+ ctx->max_parallel_connections =
+ client_set->max_parallel_connections;
+
+ ctx->to = timeout_add(2000,
+ test_client_request_timed_out2_timeout, ctx);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out2-1.txt",
+ test_client_request_timed_out2_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_attempt_timeout_msecs(hreq, 1000);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out2-2.txt",
+ test_client_request_timed_out2_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_attempt_timeout_msecs(hreq, 1000);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_request_timed_out(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("request timed out: one attempt");
+ http_client_set.request_timeout_msecs = 1000;
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request timed out: two attempts");
+ http_client_set.request_timeout_msecs = 1000;
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request absolutely timed out");
+ http_client_set.request_timeout_msecs = 0;
+ http_client_set.request_absolute_timeout_msecs = 2000;
+ http_client_set.max_attempts = 3;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request double timed out");
+ http_client_set.request_timeout_msecs = 500;
+ http_client_set.request_absolute_timeout_msecs = 2000;
+ http_client_set.max_attempts = 3;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request timed out: specific timeout");
+ http_client_set.request_timeout_msecs = 3000;
+ http_client_set.request_absolute_timeout_msecs = 0;
+ http_client_set.max_attempts = 1;
+ http_client_set.max_parallel_connections = 1;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out2,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request timed out: specific timeout (parallel)");
+ http_client_set.request_timeout_msecs = 3000;
+ http_client_set.request_absolute_timeout_msecs = 0;
+ http_client_set.max_attempts = 1;
+ http_client_set.max_parallel_connections = 4;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out2,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+}
+
+/*
+ * Request aborted early
+ */
+
+/* server */
+
+static void
+test_request_aborted_early_input(struct server_connection *conn ATTR_UNUSED)
+{
+ static const char *resp =
+ "HTTP/1.1 404 Not Found\r\n"
+ "\r\n";
+
+ /* wait one second to respond */
+ i_sleep_intr_secs(1);
+
+ /* respond */
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_request_aborted_early(unsigned int index)
+{
+ test_server_input = test_request_aborted_early_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _request_aborted_early_ctx {
+ struct http_client_request *req1, *req2;
+ struct timeout *to;
+};
+
+static void
+test_client_request_aborted_early_response(
+ const struct http_response *resp,
+ struct _request_aborted_early_ctx *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ /* abort does not trigger callback */
+ test_assert(FALSE);
+}
+
+static void
+test_client_request_aborted_early_timeout(
+ struct _request_aborted_early_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+
+ if (ctx->req1 != NULL) {
+ /* abort early */
+ http_client_request_abort(&ctx->req1); /* sent */
+ http_client_request_abort(&ctx->req2); /* only queued */
+
+ /* wait a little for server to actually respond to an
+ already aborted request */
+ ctx->to = timeout_add_short(
+ 1000, test_client_request_aborted_early_timeout, ctx);
+ } else {
+ /* all done */
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_request_aborted_early(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _request_aborted_early_ctx *ctx;
+
+ ctx = i_new(struct _request_aborted_early_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = ctx->req1 = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-aborted-early.txt",
+ test_client_request_aborted_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = ctx->req2 = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-aborted-early2.txt",
+ test_client_request_aborted_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ ctx->to = timeout_add_short(
+ 500, test_client_request_aborted_early_timeout, ctx);
+ return TRUE;
+}
+
+/* test */
+
+static void test_request_aborted_early(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("request aborted early");
+ test_run_client_server(&http_client_set,
+ test_client_request_aborted_early,
+ test_server_request_aborted_early, 1, NULL);
+ test_end();
+}
+
+/*
+ * Request failed blocking
+ */
+
+/* server */
+
+static void
+test_request_failed_blocking_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "\r\n";
+
+ /* respond */
+ o_stream_nsend_str(conn->conn.output, resp);
+ i_sleep_intr_secs(10);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_request_failed_blocking(unsigned int index)
+{
+ test_server_input = test_request_failed_blocking_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _request_failed_blocking_ctx {
+ struct http_client_request *req;
+};
+
+static void
+test_client_request_failed_blocking_response(
+ const struct http_response *resp,
+ struct _request_failed_blocking_ctx *ctx ATTR_UNUSED)
+{
+ test_client_assert_response(resp, resp->status == 500);
+}
+
+static bool
+test_client_request_failed_blocking(
+ const struct http_client_settings *client_set)
+{
+ static const char *payload = "This a test payload!";
+ struct http_client_request *hreq;
+ struct _request_failed_blocking_ctx *ctx;
+ unsigned int n;
+ string_t *data;
+
+ data = str_new(default_pool, 1000000);
+ for (n = 0; n < 50000; n++)
+ str_append(data, payload);
+
+ ctx = i_new(struct _request_failed_blocking_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = ctx->req = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-failed-blocking.txt",
+ test_client_request_failed_blocking_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+
+ test_assert(http_client_request_send_payload(&hreq,
+ str_data(data), str_len(data)) < 0);
+ i_assert(hreq == NULL);
+
+ str_free(&data);
+ i_free(ctx);
+ return FALSE;
+}
+
+/* test */
+
+static void test_request_failed_blocking(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.socket_send_buffer_size = 4096;
+
+ test_begin("request failed blocking");
+ test_run_client_server(&http_client_set,
+ test_client_request_failed_blocking,
+ test_server_request_failed_blocking, 1, NULL);
+ test_end();
+}
+
+/*
+ * Client deinit early
+ */
+
+/* server */
+
+static void
+test_client_deinit_early_input(struct server_connection *conn ATTR_UNUSED)
+{
+ static const char *resp =
+ "HTTP/1.1 404 Not Found\r\n"
+ "\r\n";
+
+ /* wait one second to respond */
+ i_sleep_intr_secs(1);
+
+ /* respond */
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_client_deinit_early(unsigned int index)
+{
+ test_server_input = test_client_deinit_early_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _client_deinit_early_ctx {
+ struct timeout *to;
+};
+
+static void
+test_client_client_deinit_early_response(
+ const struct http_response *resp,
+ struct _client_deinit_early_ctx *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ /* abort does not trigger callback */
+ test_assert(FALSE);
+}
+
+static void
+test_client_client_deinit_early_timeout(struct _client_deinit_early_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+
+ /* deinit early */
+ http_client_deinit(&http_client);
+
+ /* all done */
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_client_deinit_early(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _client_deinit_early_ctx *ctx;
+
+ ctx = i_new(struct _client_deinit_early_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/client-deinit-early.txt",
+ test_client_client_deinit_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/client-deinit-early2.txt",
+ test_client_client_deinit_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ ctx->to = timeout_add_short(
+ 500, test_client_client_deinit_early_timeout, ctx);
+ return TRUE;
+}
+
+/* test */
+
+static void test_client_deinit_early(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("client deinit early");
+ test_run_client_server(&http_client_set,
+ test_client_client_deinit_early,
+ test_server_client_deinit_early, 1, NULL);
+ test_end();
+}
+
+/*
+ * Retry with delay
+ */
+
+/* server */
+
+static void test_retry_with_delay_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_retry_with_delay(unsigned int index)
+{
+ test_server_input = test_retry_with_delay_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _client_retry_with_delay_ctx {
+ struct http_client_request *req;
+ unsigned int retries;
+ struct timeval time;
+};
+
+static void
+test_client_retry_with_delay_response(
+ const struct http_response *resp,
+ struct _client_retry_with_delay_ctx *ctx)
+{
+ int real_delay, exp_delay;
+
+ test_client_assert_response(resp, resp->status == 500);
+
+ if (ctx->retries > 0) {
+ /* check delay */
+ real_delay = timeval_diff_msecs(&ioloop_timeval, &ctx->time);
+ exp_delay = (1 << (ctx->retries-1)) * 50;
+ if (real_delay < exp_delay-2) {
+ i_fatal("Retry delay is too short %d < %d",
+ real_delay, exp_delay);
+ }
+ }
+
+ http_client_request_delay_msecs(ctx->req, (1 << ctx->retries) * 50);
+ ctx->time = ioloop_timeval;
+ if (http_client_request_try_retry(ctx->req)) {
+ ctx->retries++;
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_retry_with_delay(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _client_retry_with_delay_ctx *ctx;
+
+ ctx = i_new(struct _client_retry_with_delay_ctx, 1);
+ ctx->time = ioloop_timeval;
+
+ http_client = http_client_init(client_set);
+
+ ctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/retry-with-delay.txt",
+ test_client_retry_with_delay_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_retry_with_delay(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_attempts = 3;
+
+ test_begin("retry with delay");
+ test_run_client_server(&http_client_set,
+ test_client_retry_with_delay,
+ test_server_retry_with_delay, 1, NULL);
+ test_end();
+}
+
+/*
+ * DNS service failure
+ */
+
+/* client */
+
+struct _dns_service_failure {
+ unsigned int count;
+};
+
+static void
+test_client_dns_service_failure_response(
+ const struct http_response *resp,
+ struct _dns_service_failure *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_service_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_service_failure *ctx;
+
+ ctx = i_new(struct _dns_service_failure, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-service-failure.txt",
+ test_client_dns_service_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-service-failure2.txt",
+ test_client_dns_service_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_service_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./frop";
+
+ test_begin("dns service failure");
+ test_run_client_server(&http_client_set,
+ test_client_dns_service_failure,
+ NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * DNS timeout
+ */
+
+/* dns */
+
+static void test_dns_timeout_input(struct server_connection *conn ATTR_UNUSED)
+{
+ /* hang */
+ i_sleep_intr_secs(100);
+ server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_timeout(void)
+{
+ test_server_input = test_dns_timeout_input;
+ test_server_run(0);
+}
+
+/* client */
+
+struct _dns_timeout {
+ unsigned int count;
+};
+
+static void
+test_client_dns_timeout_response(
+ const struct http_response *resp,
+ struct _dns_timeout *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_timeout(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_timeout *ctx;
+
+ ctx = i_new(struct _dns_timeout, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-timeout.txt",
+ test_client_dns_timeout_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-timeout2.txt",
+ test_client_dns_timeout_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_timeout(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.request_timeout_msecs = 2000;
+ http_client_set.connect_timeout_msecs = 2000;
+ http_client_set.dns_client_socket_path = "./dns-test";
+
+ test_begin("dns timeout");
+ test_run_client_server(&http_client_set,
+ test_client_dns_timeout, NULL, 0,
+ test_dns_dns_timeout);
+ test_end();
+}
+
+/*
+ * DNS lookup failure
+ */
+
+/* dns */
+
+static void
+test_dns_lookup_failure_input(struct server_connection *conn)
+{
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ o_stream_nsend_str(conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_lookup_failure(void)
+{
+ test_server_input = test_dns_lookup_failure_input;
+ test_server_run(0);
+}
+
+/* client */
+
+struct _dns_lookup_failure {
+ unsigned int count;
+};
+
+static void
+test_client_dns_lookup_failure_response(const struct http_response *resp,
+ struct _dns_lookup_failure *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_lookup_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_lookup_failure *ctx;
+
+ ctx = i_new(struct _dns_lookup_failure, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-lookup-failure.txt",
+ test_client_dns_lookup_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-lookup-failure2.txt",
+ test_client_dns_lookup_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_lookup_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+
+ test_begin("dns lookup failure");
+ test_run_client_server(&http_client_set,
+ test_client_dns_lookup_failure, NULL, 0,
+ test_dns_dns_lookup_failure);
+ test_end();
+}
+
+/*
+ * DNS lookup ttl
+ */
+
+/* dns */
+
+static void
+test_dns_lookup_ttl_input(struct server_connection *conn)
+{
+ static unsigned int count = 0;
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST %u: %s", count, line);
+
+ if (count == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.1\n");
+ } else {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ if (count > 4) {
+ server_connection_deinit(&conn);
+ return;
+ }
+ }
+ count++;
+ }
+}
+
+static void test_dns_dns_lookup_ttl(void)
+{
+ test_server_input = test_dns_lookup_ttl_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void
+test_server_dns_lookup_ttl_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_dns_lookup_ttl(unsigned int index)
+{
+ test_server_input = test_server_dns_lookup_ttl_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _dns_lookup_ttl {
+ struct http_client *client;
+ unsigned int count;
+ struct timeout *to;
+};
+
+static void
+test_client_dns_lookup_ttl_response_stage2(const struct http_response *resp,
+ struct _dns_lookup_ttl *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void test_client_dns_lookup_ttl_stage2_start(struct _dns_lookup_ttl *ctx)
+{
+ struct http_client_request *hreq;
+
+ timeout_remove(&ctx->to);
+
+ ctx->count = 2;
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl-stage2.txt",
+ test_client_dns_lookup_ttl_response_stage2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl2-stage2.txt",
+ test_client_dns_lookup_ttl_response_stage2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_dns_lookup_ttl_response_stage1(const struct http_response *resp,
+ struct _dns_lookup_ttl *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ ctx->to = timeout_add(2000,
+ test_client_dns_lookup_ttl_stage2_start, ctx);
+ }
+}
+
+static bool
+test_client_dns_lookup_ttl(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_lookup_ttl *ctx;
+
+ ctx = i_new(struct _dns_lookup_ttl, 1);
+ ctx->count = 2;
+
+ ctx->client = http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl-stage1.txt",
+ test_client_dns_lookup_ttl_response_stage1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl2-stage1.txt",
+ test_client_dns_lookup_ttl_response_stage1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_lookup_ttl(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.dns_ttl_msecs = 1000;
+
+ test_begin("dns lookup ttl");
+ test_run_client_server(&http_client_set,
+ test_client_dns_lookup_ttl,
+ test_server_dns_lookup_ttl, 1,
+ test_dns_dns_lookup_ttl);
+ test_end();
+}
+
+/*
+ * Peer reuse failure
+ */
+
+/* server */
+
+static void test_peer_reuse_failure_input(struct server_connection *conn)
+{
+ static unsigned int seq = 0;
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ if (seq++ > 2) {
+ server_connection_deinit(&conn);
+ io_loop_stop(current_ioloop);
+ }
+}
+
+static void test_server_peer_reuse_failure(unsigned int index)
+{
+ test_server_input = test_peer_reuse_failure_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _peer_reuse_failure {
+ struct timeout *to;
+ bool first:1;
+};
+
+static void
+test_client_peer_reuse_failure_response2(const struct http_response *resp,
+ struct _peer_reuse_failure *ctx)
+{
+ test_client_assert_response(
+ resp, http_response_is_internal_error(resp));
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_client_peer_reuse_failure_next(struct _peer_reuse_failure *ctx)
+{
+ struct http_client_request *hreq;
+
+ timeout_remove(&ctx->to);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/peer-reuse-next.txt",
+ test_client_peer_reuse_failure_response2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_peer_reuse_failure_response1(const struct http_response *resp,
+ struct _peer_reuse_failure *ctx)
+{
+ if (ctx->first) {
+ test_client_assert_response(resp, resp->status == 200);
+
+ ctx->first = FALSE;
+ ctx->to = timeout_add_short(
+ 500, test_client_peer_reuse_failure_next, ctx);
+ } else {
+ test_client_assert_response(
+ resp, http_response_is_internal_error(resp));
+ }
+
+ test_assert(resp->reason != NULL && *resp->reason != '\0');
+}
+
+static bool
+test_client_peer_reuse_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _peer_reuse_failure *ctx;
+
+ ctx = i_new(struct _peer_reuse_failure, 1);
+ ctx->first = TRUE;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/peer-reuse.txt",
+ test_client_peer_reuse_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/peer-reuse.txt",
+ test_client_peer_reuse_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/peer-reuse.txt",
+ test_client_peer_reuse_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_peer_reuse_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_connect_attempts = 1;
+ http_client_set.max_idle_time_msecs = 500;
+
+ test_begin("peer reuse failure");
+ test_run_client_server(&http_client_set,
+ test_client_peer_reuse_failure,
+ test_server_peer_reuse_failure, 1, NULL);
+ test_end();
+}
+
+/*
+ * Reconnect failure
+ */
+
+/* dns */
+
+static void test_dns_reconnect_failure_input(struct server_connection *conn)
+{
+ static unsigned int count = 0;
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST %u: %s", count, line);
+
+ if (count == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.1\n");
+ } else {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ if (count > 4) {
+ server_connection_deinit(&conn);
+ return;
+ }
+ }
+ count++;
+ }
+}
+
+static void test_dns_reconnect_failure(void)
+{
+ test_server_input = test_dns_reconnect_failure_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void test_reconnect_failure_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Everything is OK\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ io_loop_stop(current_ioloop);
+ io_remove(&io_listen);
+ i_close_fd(&fd_listen);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_reconnect_failure(unsigned int index)
+{
+ test_server_input = test_reconnect_failure_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _reconnect_failure_ctx {
+ struct timeout *to;
+};
+
+static void
+test_client_reconnect_failure_response2(const struct http_response *resp,
+ struct _reconnect_failure_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ io_loop_stop(ioloop);
+ i_free(ctx);
+}
+
+static void
+test_client_reconnect_failure_next(struct _reconnect_failure_ctx *ctx)
+{
+ struct http_client_request *hreq;
+
+ if (debug)
+ i_debug("NEXT REQUEST");
+
+ timeout_remove(&ctx->to);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/reconnect-failure-2.txt",
+ test_client_reconnect_failure_response2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_reconnect_failure_response1(const struct http_response *resp,
+ struct _reconnect_failure_ctx *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ ctx->to = timeout_add_short(
+ 5000, test_client_reconnect_failure_next, ctx);
+}
+
+static bool
+test_client_reconnect_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _reconnect_failure_ctx *ctx;
+
+ ctx = i_new(struct _reconnect_failure_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/reconnect-failure-1.txt",
+ test_client_reconnect_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_reconnect_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.dns_ttl_msecs = 10000;
+ http_client_set.max_idle_time_msecs = 1000;
+ http_client_set.max_attempts = 1;
+ http_client_set.request_timeout_msecs = 1000;
+
+ test_begin("reconnect failure");
+ test_run_client_server(&http_client_set,
+ test_client_reconnect_failure,
+ test_server_reconnect_failure, 1,
+ test_dns_reconnect_failure);
+ test_end();
+}
+
+/*
+ * Multi IP attempts
+ */
+
+/* dns */
+
+static void test_multi_ip_attempts_input(struct server_connection *conn)
+{
+ unsigned int count = 0;
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST %u: %s", count, line);
+
+ if (strcmp(line, "IP\ttest1.local") == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.4\t127.0.0.3\t"
+ "127.0.0.2\t127.0.0.1\n");
+ continue;
+ }
+
+ o_stream_nsend_str(conn->conn.output,
+ "0\t10.255.255.1\t192.168.0.0\t"
+ "192.168.255.255\t127.0.0.1\n");
+ }
+}
+
+static void test_dns_multi_ip_attempts(void)
+{
+ test_server_input = test_multi_ip_attempts_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void test_server_multi_ip_attempts_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_multi_ip_attempts(unsigned int index)
+{
+ test_server_input = test_server_multi_ip_attempts_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _multi_ip_attempts {
+ unsigned int count;
+};
+
+static void
+test_client_multi_ip_attempts_response(const struct http_response *resp,
+ struct _multi_ip_attempts *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_multi_ip_attempts1(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _multi_ip_attempts *ctx;
+
+ ctx = i_new(struct _multi_ip_attempts, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "test1.local", "/multi-ip-attempts.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "test1.local", "/multi-ip-attempts2.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+static bool
+test_client_multi_ip_attempts2(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _multi_ip_attempts *ctx;
+
+ ctx = i_new(struct _multi_ip_attempts, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "test2.local", "/multi-ip-attempts.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "test2.local", "/multi-ip-attempts2.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_multi_ip_attempts(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.connect_timeout_msecs = 1000;
+ http_client_set.request_timeout_msecs = 1000;
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.max_connect_attempts = 4;
+
+ test_begin("multi IP attempts (connection refused)");
+ test_run_client_server(&http_client_set,
+ test_client_multi_ip_attempts1,
+ test_server_multi_ip_attempts, 1,
+ test_dns_multi_ip_attempts);
+ test_end();
+
+ test_begin("multi IP attempts (connect timeout)");
+ test_run_client_server(&http_client_set,
+ test_client_multi_ip_attempts2,
+ test_server_multi_ip_attempts, 1,
+ test_dns_multi_ip_attempts);
+ test_end();
+
+ http_client_set.soft_connect_timeout_msecs = 100;
+
+ test_begin("multi IP attempts (soft connect timeout)");
+ test_run_client_server(&http_client_set,
+ test_client_multi_ip_attempts2,
+ test_server_multi_ip_attempts, 1,
+ test_dns_multi_ip_attempts);
+ test_end();
+}
+
+/*
+ * Idle connections
+ */
+
+/* server */
+
+struct _idle_connections_sctx {
+ bool eoh;
+};
+
+static int test_idle_connections_init(struct server_connection *conn)
+{
+ struct _idle_connections_sctx *ctx;
+
+ ctx = p_new(conn->pool, struct _idle_connections_sctx, 1);
+ conn->context = ctx;
+ return 0;
+}
+
+static void test_idle_connections_input(struct server_connection *conn)
+{
+ struct _idle_connections_sctx *ctx = conn->context;
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (*line == '\0') {
+ ctx->eoh = TRUE;
+ break;
+ }
+ }
+
+ if (conn->conn.input->stream_errno != 0) {
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ i_assert(ctx->eoh);
+ ctx->eoh = FALSE;
+
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ if (o_stream_flush(conn->conn.output) < 0) {
+ i_fatal("server: Flush error: %s",
+ o_stream_get_error(conn->conn.output));
+ }
+}
+
+static void test_server_idle_connections(unsigned int index)
+{
+ test_server_init = test_idle_connections_init;
+ test_server_input = test_idle_connections_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _idle_connections {
+ struct http_client *client;
+ unsigned int max, count;
+ struct timeout *to;
+};
+
+static void
+test_client_idle_connections_response_stage2(const struct http_response *resp,
+ struct _idle_connections *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void test_client_idle_connections_stage2_start(struct _idle_connections *ctx)
+{
+ struct http_client_request *hreq;
+ unsigned int i;
+
+ if (debug)
+ i_debug("STAGE 2");
+
+ timeout_remove(&ctx->to);
+
+ ctx->count = ctx->max;
+
+ for (i = 0; i < ctx->count; i++) {
+ hreq = http_client_request(
+ ctx->client, "GET", net_ip2addr(&bind_ip),
+ t_strdup_printf("/idle-connections-stage2-%d.txt", i),
+ test_client_idle_connections_response_stage2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+ }
+}
+
+static void
+test_client_idle_connections_response_stage1(const struct http_response *resp,
+ struct _idle_connections *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ if (debug)
+ i_debug("START STAGE 2");
+ ctx->to = timeout_add_short(
+ 550, test_client_idle_connections_stage2_start, ctx);
+ }
+}
+
+static bool
+test_client_idle_connections(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _idle_connections *ctx;
+ unsigned int i;
+
+ if (debug)
+ i_debug("STAGE 1");
+
+ ctx = i_new(struct _idle_connections, 1);
+ ctx->max = client_set->max_parallel_connections;
+ ctx->count = client_set->max_parallel_connections;
+
+ ctx->client = http_client = http_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++) {
+ hreq = http_client_request(
+ ctx->client, "GET", net_ip2addr(&bind_ip),
+ t_strdup_printf("/idle-connections-stage1-%d.txt", i),
+ test_client_idle_connections_response_stage1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+ }
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_idle_connections(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_idle_time_msecs = 1000;
+
+ test_begin("idle connections (max 1)");
+ http_client_set.max_parallel_connections = 1;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+
+ test_begin("idle connections (max 2)");
+ http_client_set.max_parallel_connections = 2;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+
+ test_begin("idle connections (max 4)");
+ http_client_set.max_parallel_connections = 4;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+
+ test_begin("idle connections (max 8)");
+ http_client_set.max_parallel_connections = 8;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+}
+
+/*
+ * Idle hosts
+ */
+
+/* dns */
+
+static void
+test_dns_idle_hosts_input(struct server_connection *conn)
+{
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST: %s", line);
+
+ if (strcmp(line, "IP\thosta") == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.1\n");
+ } else {
+ i_sleep_msecs(300);
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ }
+ }
+}
+
+static void test_dns_idle_hosts(void)
+{
+ test_server_input = test_dns_idle_hosts_input;
+ test_server_run(0);
+}
+
+/* server */
+
+struct _idle_hosts_sctx {
+ bool eoh;
+};
+
+static int test_idle_hosts_init(struct server_connection *conn)
+{
+ struct _idle_hosts_sctx *ctx;
+
+ ctx = p_new(conn->pool, struct _idle_hosts_sctx, 1);
+ conn->context = ctx;
+ return 0;
+}
+
+static void test_idle_hosts_input(struct server_connection *conn)
+{
+ struct _idle_hosts_sctx *ctx = conn->context;
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (*line == '\0') {
+ ctx->eoh = TRUE;
+ break;
+ }
+ }
+
+ if (conn->conn.input->stream_errno != 0) {
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ i_assert(ctx->eoh);
+ ctx->eoh = FALSE;
+
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ if (o_stream_flush(conn->conn.output) < 0) {
+ i_fatal("server: Flush error: %s",
+ o_stream_get_error(conn->conn.output));
+ }
+}
+
+static void test_server_idle_hosts(unsigned int index)
+{
+ test_server_init = test_idle_hosts_init;
+ test_server_input = test_idle_hosts_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _idle_hosts {
+ struct http_client *client;
+ struct http_client_request *hostb_req;
+ unsigned int count;
+};
+
+static void
+test_client_idle_hosts_response_hosta(const struct http_response *resp,
+ struct _idle_hosts *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void
+test_client_idle_hosts_response_hostb(const struct http_response *resp,
+ struct _idle_hosts *ctx)
+{
+ test_client_assert_response(resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (http_client_request_try_retry(ctx->hostb_req)) {
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+
+ ctx->hostb_req = NULL;
+}
+
+static bool
+test_client_idle_hosts(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _idle_hosts *ctx;
+
+ ctx = i_new(struct _idle_hosts, 1);
+ ctx->count = 2;
+
+ ctx->client = http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "hosta",
+ t_strdup_printf("/idle-hosts-a1.txt"),
+ test_client_idle_hosts_response_hosta, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = ctx->hostb_req = http_client_request(
+ ctx->client, "GET", "hostb",
+ t_strdup_printf("/idle-hosts-b.txt"),
+ test_client_idle_hosts_response_hostb, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "hosta",
+ t_strdup_printf("/idle-hosts-a2.txt"),
+ test_client_idle_hosts_response_hosta, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_delay_msecs(hreq, 600);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_idle_hosts(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.dns_ttl_msecs = 400;
+ http_client_set.max_parallel_connections = 1;
+ http_client_set.max_idle_time_msecs = 100;
+ http_client_set.max_attempts = 2;
+
+ test_begin("idle hosts");
+ test_run_client_server(&http_client_set,
+ test_client_idle_hosts,
+ test_server_idle_hosts, 1,
+ test_dns_idle_hosts);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_unconfigured_ssl,
+ test_unconfigured_ssl_abort,
+ test_invalid_url,
+ test_host_lookup_failed,
+ test_connection_refused,
+ test_connection_lost_prematurely,
+ test_connection_timed_out,
+ test_invalid_redirect,
+ test_unseekable_redirect,
+ test_unseekable_retry,
+ test_broken_payload,
+ test_retry_payload,
+ test_connection_lost,
+ test_connection_lost_100,
+ test_connection_lost_sub_ioloop,
+ test_early_success,
+ test_bad_response,
+ test_request_timed_out,
+ test_request_aborted_early,
+ test_request_failed_blocking,
+ test_client_deinit_early,
+ test_retry_with_delay,
+ test_dns_service_failure,
+ test_dns_timeout,
+ test_dns_lookup_failure,
+ test_dns_lookup_ttl,
+ test_peer_reuse_failure,
+ test_reconnect_failure,
+ test_multi_ip_attempts,
+ test_idle_connections,
+ test_idle_hosts,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_defaults(struct http_client_settings *http_set)
+{
+ /* client settings */
+ i_zero(http_set);
+ http_set->max_idle_time_msecs = 5*1000;
+ http_set->max_parallel_connections = 1;
+ http_set->max_pipelined_requests = 1;
+ http_set->max_redirects = 0;
+ http_set->max_attempts = 1;
+ http_set->debug = debug;
+}
+
+static void test_client_progress_timeout(void *context ATTR_UNUSED)
+{
+ /* Terminate test due to lack of progress */
+ test_assert(FALSE);
+ timeout_remove(&to_client_progress);
+ io_loop_stop(current_ioloop);
+}
+
+static bool
+test_client_init(test_client_init_t client_test,
+ const struct http_client_settings *client_set)
+{
+ i_assert(client_test != NULL);
+ if (!client_test(client_set))
+ return FALSE;
+
+ to_client_progress = timeout_add(CLIENT_PROGRESS_TIMEOUT*1000,
+ test_client_progress_timeout, NULL);
+ return TRUE;
+}
+
+static void test_client_deinit(void)
+{
+ timeout_remove(&to_client_progress);
+
+ if (http_client != NULL)
+ http_client_deinit(&http_client);
+}
+
+static void
+test_client_run(test_client_init_t client_test,
+ const struct http_client_settings *client_set)
+{
+ if (test_client_init(client_test, client_set))
+ io_loop_run(ioloop);
+ test_client_deinit();
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static void server_connection_input(struct connection *_conn)
+{
+ struct server_connection *conn = (struct server_connection *)_conn;
+
+ test_server_input(conn);
+}
+
+static void server_connection_init(int fd)
+{
+ struct server_connection *conn;
+ pool_t pool;
+
+ net_set_nonblock(fd, TRUE);
+
+ pool = pool_alloconly_create("server connection", 512);
+ conn = p_new(pool, struct server_connection, 1);
+ conn->pool = pool;
+
+ connection_init_server(server_conn_list, &conn->conn,
+ "server connection", fd, fd);
+
+ if (test_server_init != NULL) {
+ if (test_server_init(conn) != 0)
+ return;
+ }
+}
+
+static void server_connection_deinit(struct server_connection **_conn)
+{
+ struct server_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (test_server_deinit != NULL)
+ test_server_deinit(conn);
+
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void server_connection_destroy(struct connection *_conn)
+{
+ struct server_connection *conn = (struct server_connection *)_conn;
+
+ server_connection_deinit(&conn);
+}
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ /* accept new client */
+ fd = net_accept(fd_listen, NULL, NULL);
+ if (fd == -1)
+ return;
+ if (fd == -2) {
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ server_connection_init(fd);
+}
+
+/* */
+
+static struct connection_settings server_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs server_connection_vfuncs = {
+ .destroy = server_connection_destroy,
+ .input = server_connection_input
+};
+
+static void test_server_run(unsigned int index)
+{
+ server_index = index;
+
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);
+
+ server_conn_list = connection_list_init(&server_connection_set,
+ &server_connection_vfuncs);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+
+ connection_list_deinit(&server_conn_list);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+ unsigned int index;
+ test_server_init_t server_test;
+};
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+ int fd = net_listen(&bind_ip, bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", *bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), *bind_port);
+ }
+ return fd;
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+ i_set_failure_prefix("SERVER[%u]: ", data->index + 1);
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ test_subprocess_notify_signal_send_parent(SIGHUP);
+ ioloop = io_loop_create();
+ data->server_test(data->index);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ main_deinit();
+ return 0;
+}
+
+static int test_run_dns(test_dns_init_t dns_test)
+{
+ i_set_failure_prefix("DNS: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ dns_test();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_client(const struct http_client_settings *client_set,
+ test_client_init_t client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ test_client_run(client_test, client_set);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct http_client_settings *client_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count,
+ test_dns_init_t dns_test)
+{
+ unsigned int i;
+
+ test_subprocess_notify_signal_reset(SIGHUP);
+ test_server_init = NULL;
+ test_server_deinit = NULL;
+ test_server_input = NULL;
+
+ if (server_tests_count > 0) {
+ int fds[server_tests_count];
+
+ bind_ports = i_new(in_port_t, server_tests_count);
+ for (i = 0; i < server_tests_count; i++)
+ fds[i] = test_open_server_fd(&bind_ports[i]);
+
+ for (i = 0; i < server_tests_count; i++) {
+ struct test_server_data data;
+
+ i_zero(&data);
+ data.index = i;
+ data.server_test = server_test;
+
+ /* Fork server */
+ fd_listen = fds[i];
+ test_subprocess_fork(test_run_server, &data, FALSE);
+ i_close_fd(&fd_listen);
+ test_subprocess_notify_signal_wait(SIGHUP, 10000);
+ test_subprocess_notify_signal_reset(SIGHUP);
+ }
+ }
+
+ if (dns_test != NULL) {
+ int fd;
+
+ i_unlink_if_exists("./dns-test");
+ fd = net_listen_unix("./dns-test", 128);
+ if (fd == -1) {
+ i_fatal("listen(./dns-test) failed: %m");
+ }
+
+ /* Fork DNS service */
+ fd_listen = fd;
+ test_subprocess_fork(test_run_dns, dns_test, FALSE);
+ i_close_fd(&fd_listen);
+ }
+
+ /* Run client */
+ test_run_client(client_set, client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ i_free(bind_ports);
+
+ i_unlink_if_exists("./dns-test");
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+ /* nothing yet */
+}
+
+static void main_deinit(void)
+{
+ /* nothing yet; also called from sub-processes */
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}