/*
 * lws-minimal-http-client-openhitls-sni-multivhost-positive
 *
 * Written in 2010-2026 by Andy Green <andy@warmcat.com>
 *
 * This file is made available under the Creative Commons CC0 1.0
 * Universal Public Domain Dedication.
 *
 * Verifies successful SNI-based vhost and certificate selection with
 * openHiTLS.  Two TLS vhosts share a single listen port, and the client
 * connects to each by name while dialing 127.0.0.1.
 */

#include <libwebsockets.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define DEFAULT_TEST_PORT 7830
#define TEST_CA_BUNDLE "openhitls-sni-ca-bundle.pem"
#define TEST_CONNECT_ADDRESS "127.0.0.1"
#define TEST_LOCALHOST_CERT \
	"../minimal-http-client-openhitls-mtls-file-positive/mtls-file-server.cert"
#define TEST_LOCALHOST_KEY \
	"../minimal-http-client-openhitls-mtls-file-positive/mtls-file-server.key"
#define TEST_WRONGHOST_CERT \
	"../minimal-http-client-openhitls-certfail/wronghost.example.com.cert"
#define TEST_WRONGHOST_KEY \
	"../minimal-http-client-openhitls-certfail/wronghost.example.com.key"

struct pss_http {
	char body[LWS_PRE + 128];
	size_t body_len;
};

struct sni_case {
	const char *server_name;
	const char *expected_cn;
	const char *expected_body;
};

static const struct sni_case sni_cases[] = {
	{
		"localhost",
		"localhost",
		"served-by localhost\n"
	},
	{
		"wronghost.example.com",
		"wronghost.example.com",
		"served-by wronghost.example.com\n"
	}
};

static struct lws_context *context;
static struct lws_vhost *client_vhosts[LWS_ARRAY_SIZE(sni_cases)];
static struct lws *client_wsi;
static lws_sorted_usec_list_t sul_next;
static int interrupted, bad, completed, response_status;
static int test_port = DEFAULT_TEST_PORT;
static unsigned int current_case;
static unsigned int peer_verified;
static char response_body[128];
static size_t response_body_len;
static char peer_cn[128];

static void
fail_test(const char *why)
{
	lwsl_err("%s\n", why);
	bad = 1;
	completed = 1;
	if (context) {
		lws_cancel_service(context);
	}
}

static void
reset_case_state(void)
{
	response_status = 0;
	response_body_len = 0;
	response_body[0] = '\0';
	peer_cn[0] = '\0';
	peer_verified = 0;
}

static int
copy_cert_cn(struct lws *wsi, char *dest, size_t dest_len, unsigned int *verified)
{
	union lws_tls_cert_info_results ir;

	if (lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_COMMON_NAME, &ir,
				   sizeof(ir.ns.name))) {
		return -1;
	}

	lws_strncpy(dest, ir.ns.name, dest_len);

	if (lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_VERIFIED, &ir, 0)) {
		return -1;
	}

	*verified = ir.verified;

	return 0;
}

static int
start_client(void);

static void
start_client_cb(lws_sorted_usec_list_t *sul)
{
	(void)sul;

	if (start_client()) {
		fail_test("failed to start client connection");
	}
}

static void
complete_case(struct lws *wsi)
{
	const struct sni_case *tc;

	(void)wsi;

	tc = &sni_cases[current_case];

	if (response_status != HTTP_STATUS_OK) {
		fail_test("unexpected HTTP status");
		return;
	}

	if (!peer_verified) {
		fail_test("peer certificate was not verified");
		return;
	}

	if (strcmp(peer_cn, tc->expected_cn)) {
		fail_test("unexpected peer certificate CN");
		return;
	}

	if (strcmp(response_body, tc->expected_body)) {
		fail_test("unexpected response body");
		return;
	}

	lwsl_user("validated SNI '%s' -> CN '%s' body '%s'\n",
		  tc->server_name, peer_cn, response_body);

	current_case++;
	if (current_case == LWS_ARRAY_SIZE(sni_cases)) {
		completed = 1;
		lws_cancel_service(context);
		return;
	}

	reset_case_state();
	lws_sul_schedule(context, 0, &sul_next, start_client_cb,
			 100 * LWS_US_PER_MS);
}

static int
callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
	      void *in, size_t len)
{
	struct pss_http *pss = (struct pss_http *)user;
	uint8_t headers[LWS_PRE + LWS_RECOMMENDED_MIN_HEADER_SPACE],
		*start = &headers[LWS_PRE], *p = start,
		*end = &headers[sizeof(headers) - 1];

	switch (reason) {
	case LWS_CALLBACK_HTTP:
	{
		const char *vhost_name = lws_get_vhost_name(lws_get_vhost(wsi));
		int n;

		n = snprintf(&pss->body[LWS_PRE],
			     sizeof(pss->body) - LWS_PRE,
			     "served-by %s\n", vhost_name ? vhost_name : "?");
		if (n < 0 || (size_t)n >= sizeof(pss->body) - LWS_PRE) {
			return 1;
		}
		pss->body_len = (size_t)n;

		lwsl_user("server routed request to vhost '%s'\n",
			  vhost_name ? vhost_name : "(null)");

		if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "text/plain",
						(lws_filepos_t)pss->body_len,
						&p, end)) {
			return 1;
		}

		if (lws_finalize_write_http_header(wsi, start, &p, end)) {
			return 1;
		}

		lws_callback_on_writable(wsi);
		return 0;
	}

	case LWS_CALLBACK_HTTP_WRITEABLE:
		if (lws_write(wsi, (unsigned char *)&pss->body[LWS_PRE],
			      (unsigned int)pss->body_len,
			      LWS_WRITE_HTTP_FINAL) != (int)pss->body_len) {
			return 1;
		}

		if (lws_http_transaction_completed(wsi)) {
			return -1;
		}
		return 0;

	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
		lwsl_err("CLIENT_CONNECTION_ERROR: %s\n",
			 in ? (const char *)in : "(null)");
		client_wsi = NULL;
		fail_test("client handshake failed");
		return 0;

	case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
		response_status = (int)lws_http_client_http_response(wsi);
		if (copy_cert_cn(wsi, peer_cn, sizeof(peer_cn), &peer_verified)) {
			fail_test("failed to read peer certificate info");
			return 0;
		}

		lwsl_user("client observed CN '%s' verified=%u status=%d for '%s'\n",
			  peer_cn, peer_verified, response_status,
			  sni_cases[current_case].server_name);
		return 0;

	case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:
		if (response_body_len + len >= sizeof(response_body)) {
			fail_test("response body exceeded test buffer");
			return -1;
		}

		memcpy(response_body + response_body_len, in, len);
		response_body_len += len;
		response_body[response_body_len] = '\0';
		return 0;

	case LWS_CALLBACK_RECEIVE_CLIENT_HTTP:
	{
		unsigned char buf[LWS_PRE + 128], *pp = &buf[LWS_PRE];
		int n = (int)sizeof(buf) - LWS_PRE;

		if (lws_http_client_read(wsi, (char **)&pp, &n) < 0) {
			fail_test("lws_http_client_read failed");
			return -1;
		}

		return 0;
	}

	case LWS_CALLBACK_COMPLETED_CLIENT_HTTP:
		if (wsi != client_wsi) {
			return 0;
		}
		client_wsi = NULL;
		if (!completed) {
			complete_case(wsi);
		}
		return 0;

	case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
		if (wsi != client_wsi) {
			return 0;
		}
		client_wsi = NULL;
		if (!completed) {
			fail_test("client connection closed before completion");
		}
		return 0;

	default:
		break;
	}

	return lws_callback_http_dummy(wsi, reason, user, in, len);
}

static const struct lws_protocols protocols[] = {
	{
		"http",
		callback_http,
		sizeof(struct pss_http),
		0, 0, NULL, 0
	},
	LWS_PROTOCOL_LIST_TERM
};

static void
sigint_handler(int sig)
{
	(void)sig;

	interrupted = 1;
	if (context) {
		lws_cancel_service(context);
	}
}

static int
start_client(void)
{
	struct lws_client_connect_info i;
	const struct sni_case *tc = &sni_cases[current_case];

	memset(&i, 0, sizeof(i));
	i.context = context;
	i.vhost = client_vhosts[current_case];
	i.address = TEST_CONNECT_ADDRESS;
	i.host = tc->server_name;
	i.origin = tc->server_name;
	i.path = "/";
	i.method = "GET";
	i.port = test_port;
	i.ssl_connection = LCCSCF_USE_SSL;
	i.protocol = protocols[0].name;
	i.pwsi = &client_wsi;

	lwsl_user("connecting to https://%s:%d/ via %s\n",
		  tc->server_name, i.port, i.address);

	if (!lws_client_connect_via_info(&i)) {
		lwsl_err("lws_client_connect_via_info failed\n");
		return -1;
	}

	return 0;
}

static struct lws_vhost *
create_tls_vhost(const char *vhost_name, const char *cert_path,
		 const char *key_path, const char *client_ca_path,
		 int client_trust_vhost)
{
	struct lws_context_creation_info info;

	memset(&info, 0, sizeof(info));
	info.port = test_port;
	info.protocols = protocols;
	info.vhost_name = vhost_name;
	info.ssl_cert_filepath = cert_path;
	info.ssl_private_key_filepath = key_path;
	info.client_ssl_ca_filepath = client_ca_path;
	if (client_trust_vhost) {
		info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
			       LWS_SERVER_OPTION_DISABLE_OS_CA_CERTS |
			       LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE;
	}

	return lws_create_vhost(context, &info);
}

int main(int argc, const char **argv)
{
	struct lws_context_creation_info info;
	const char *p;
	int n = 0;
	int ret = 1;

	signal(SIGINT, sigint_handler);
	signal(SIGTERM, sigint_handler);

	memset(&info, 0, sizeof(info));
	lws_cmdline_option_handle_builtin(argc, argv, &info);

	p = lws_cmdline_option(argc, argv, "--port");
	if (p) {
		test_port = atoi(p);
	}

	lwsl_user("LWS minimal http client openhitls sni multivhost positive\n");

	memset(&info, 0, sizeof(info));
	info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
		       LWS_SERVER_OPTION_EXPLICIT_VHOSTS;

	context = lws_create_context(&info);
	if (!context) {
		lwsl_err("lws init failed\n");
		return 1;
	}

	client_vhosts[0] = create_tls_vhost("localhost",
					    TEST_LOCALHOST_CERT,
					    TEST_LOCALHOST_KEY,
					    TEST_CA_BUNDLE, 1);
	if (!client_vhosts[0]) {
		lwsl_err("failed to create localhost vhost\n");
		goto done;
	}

	client_vhosts[1] = create_tls_vhost("wronghost.example.com",
					    TEST_WRONGHOST_CERT,
					    TEST_WRONGHOST_KEY,
					    TEST_CA_BUNDLE, 1);
	if (!client_vhosts[1]) {
		lwsl_err("failed to create wronghost vhost\n");
		goto done;
	}

	reset_case_state();
	lws_sul_schedule(context, 0, &sul_next, start_client_cb,
			 10 * LWS_US_PER_MS);

	while (n >= 0 && !interrupted && !completed) {
		n = lws_service(context, 0);
	}

	if (interrupted && !completed) {
		goto done;
	}

	lwsl_user("Completed: %s\n", bad ? "failed" : "OK");
	ret = bad ? 1 : 0;

done:
	/*
	 * The explicit multi-vhost openHiTLS teardown currently aborts after
	 * the successful SNI coverage path completes.  Exit the short-lived
	 * test process directly once the assertions are finished.
	 */
	return ret;
}
