commit cca48439b82f6d21c9b82a6b85d8e5a0b70335e6
parent abb56215377d20c1d6e9a46db634dcb94b90a4ab
Author: Evgeny Grin (Karlson2k) <k2k@drgrin.dev>
Date: Mon, 31 Aug 2026 20:46:18 +0200
Added complex test for ACME ALPN
Diffstat:
5 files changed, 2031 insertions(+), 0 deletions(-)
diff --git a/configure.ac b/configure.ac
@@ -9985,6 +9985,7 @@ src/tests/unit/Makefile
src/tests/basic/Makefile
src/tests/upgrade/Makefile
src/tests/raw/Makefile
+src/tests/acme/Makefile
src/tests/client_server/Makefile
src/examples2/Makefile
src/examples2/acme/Makefile
diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am
@@ -2,6 +2,12 @@
SUBDIRS = unit basic raw
+if MHD_SUPPORT_THREADS
+if MHD_SUPPORT_ACME
+SUBDIRS += acme
+endif
+endif
+
if HAVE_POSIX_THREADS
if MHD_SUPPORT_UPGRADE
SUBDIRS += upgrade
diff --git a/src/tests/acme/Makefile.am b/src/tests/acme/Makefile.am
@@ -0,0 +1,38 @@
+# This Makefile.am is in the public domain
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/include \
+ -I$(top_srcdir)/src/incl_priv \
+ -I$(top_srcdir)/src/mhd2 \
+ -DMHD_CPU_COUNT=$(CPU_COUNT) \
+ $(CPPFLAGS_ac) $(GNUTLS_CPPFLAGS)
+
+AM_CFLAGS = $(CFLAGS_ac) $(PTHREAD_CFLAGS)
+
+AM_LDFLAGS = $(LDFLAGS_ac) $(GNUTLS_LDFLAGS)
+
+LIBS = @LIBS@ $(PTHREAD_LIBS)
+
+AM_TESTS_ENVIRONMENT = $(TESTS_ENVIRONMENT_ac)
+
+if USE_COVERAGE
+ AM_CFLAGS += -fprofile-arcs -ftest-coverage
+endif
+
+LDADD = \
+ $(top_builddir)/src/mhd2/libmicrohttpd2.la $(GNUTLS_LIBS)
+
+$(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile
+ @echo ' cd $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la'; \
+ $(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la
+
+# The test client is implemented with GnuTLS. The daemon side is tested with
+# every TLS backend built in.
+if MHD_SUPPORT_GNUTLS
+check_PROGRAMS = \
+ test_acme_alpn
+
+test_acme_alpn_SOURCES = \
+ test_acme_alpn.c acme_test_certs.h
+endif
+
+TESTS = $(check_PROGRAMS)
diff --git a/src/tests/acme/acme_test_certs.h b/src/tests/acme/acme_test_certs.h
@@ -0,0 +1,312 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
+/*
+ This file is part of GNU libmicrohttpd.
+ Copyright (C) 2026 Evgeny Grin (Karlson2k)
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ Alternatively, you can redistribute GNU libmicrohttpd and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of
+ the License, or (at your option) any later version, together
+ with the eCos exception, as follows:
+
+ As a special exception, if other files instantiate templates or
+ use macros or inline functions from this file, or you compile this
+ file and link it with other works to produce a work based on this
+ file, this file does not by itself cause the resulting work to be
+ covered by the GNU General Public License. However the source code
+ for this file must still be made available in accordance with
+ section (3) of the GNU General Public License v2.
+
+ This exception does not invalidate any other reasons why a work
+ based on this file might be covered by the GNU General Public
+ License.
+
+ You should have received copies of the GNU Lesser General Public
+ License and the GNU General Public License along with this library;
+ if not, see <https://www.gnu.org/licenses/>.
+*/
+
+/**
+ * @file src/tests/acme/acme_test_certs.h
+ * @brief Certificates and keys for the ACME TLS-ALPN-01 challenge tests
+ * @author Karlson2k (Evgeny Grin)
+ *
+ * All certificates are self-signed, valid for one hundred years and share
+ * the same subject prefix "C=CH, ST=Geneva, L=Geneva, O=test-libmicrohttpd".
+ *
+ * The "server" certificate is the reply for any non-ACME connection.
+ * The two "challenge" certificates are the replies for the ACME TLS-ALPN-01
+ * challenge on two different domains. Challenge certificate "B" repeats
+ * the common name of the "server" certificate and uses a completely
+ * different type of the key, so that the served certificate cannot be
+ * identified by the name or by the key type alone.
+ *
+ * The challenge certificates are formed as required by RFC 8737, section 3:
+ * the subjectAltName extension has a single dNSName entry and the critical
+ * acmeIdentifier extension (OID 1.3.6.1.5.5.7.1.31) holds the DER encoded
+ * OCTET STRING with the SHA-256 digest of the key authorization. The key
+ * authorization is the challenge token joined by a dot with the account
+ * thumbprint "nRbYuckWQyMDAm6D2H4p7ZiK9yT6R8UUD0vZNWqv8ps" (synthetic).
+ * The tokens are the examples from RFC 8555:
+ * "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" for "A" and
+ * "DGyRejmCefe7v4NfDGDKfA" for "B".
+ */
+
+#ifndef MHD_ACME_TEST_CERTS_H
+#define MHD_ACME_TEST_CERTS_H 1
+
+/**
+ * The domain validated by the challenge certificate "A"
+ */
+#define mhdt_ACME_CHLNG_A_DOMAIN "acme-test1.example"
+
+/**
+ * The domain validated by the challenge certificate "B"
+ */
+#define mhdt_ACME_CHLNG_B_DOMAIN "acme-test2.example"
+
+
+/* *INDENT-OFF* */
+/**
+ * The certificate for all non-ACME connections.
+ * RSA 4096, CN=test-mhd-server, SAN: DNS:localhost, IP:127.0.0.1, IP:[::1]
+ */
+#define mhdt_ACME_SRV_CERT_PEM \
+ "-----BEGIN CERTIFICATE-----\n" \
+ "MIIGATCCA+mgAwIBAgIUZ9L0ZZRAMrVCCQFFzX/Xp5LwJJ8wDQYJKoZIhvcNAQEL\n" \
+ "BQAwZjELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu\n" \
+ "ZXZhMRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3Qt\n" \
+ "bWhkLXNlcnZlcjAgFw0yNjA4MzExNDM3MjBaGA8yMTI2MDkwMTE0MzcyMFowZjEL\n" \
+ "MAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRsw\n" \
+ "GQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3QtbWhkLXNl\n" \
+ "cnZlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANNdODgKFQX1QYKO\n" \
+ "iH8J1j/iUXds+qXpm5LKOW1lPsYOTPEUj9DmzBmjjJa6OE+5W3W7BHWEfNF7AOJb\n" \
+ "ZN9WyOtdcJh4ctBdxHi0IVGkDME2Q7HSF8GS7chXLolGUZ2/vZvgZXDYa1pPH3v2\n" \
+ "pmFS8g/D8puRpV6yuPaIF5N2yK653hDll7wJQtsOIllWa9pS93MkdOvhezzxOpOh\n" \
+ "hRlG+UMuizlAvZDBaF/ScztLoykiz5YbC4oqe8Yc71+q2uLcjab//Tg7bBLIszrn\n" \
+ "ftXqWNzCz/NuO7g3//E2KAn/CQ+AOUXrr31yn9WjL2VVnIlsoSPOQzHg4uAJCQiU\n" \
+ "PDb429E6p2r8GF/EIsa9i4Si0xBeaS5q1p0zCPUFwn5OQp+2LbkjkYarxz9/1xO7\n" \
+ "uNj/MuEGnz+ikbD1G38NopFheVZPhSNMt5w9pEMbOMwGa0HzCqodt3P3QRbRXJyt\n" \
+ "Lvc9bnRRtmCoH5deZGey2e0bMD/hO3D+Mgwm0X0eYUTi8yHIzNGcOLuNcE1a1apT\n" \
+ "rRfoPzaBetbsihcbolRbuVVezeapU75ttjH9xozfXQhU1IyoNn4ZwaefOCOoaIY5\n" \
+ "FqPc7AbaUQnUGPgfc+FGABzvHqpZ060zJjOKMkdBkNaovIXZdLcrObtbxFZO93gq\n" \
+ "o5sVgOxQpgtsZQumORH4KEogHvjdAgMBAAGjgaQwgaEwHwYDVR0jBBgwFoAULOqQ\n" \
+ "ne2Tsakom6X4bKgDgb1gDeQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAw\n" \
+ "EwYDVR0lBAwwCgYIKwYBBQUHAwEwLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAAB\n" \
+ "hxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQWBBQs6pCd7ZOxqSibpfhsqAOBvWAN\n" \
+ "5DANBgkqhkiG9w0BAQsFAAOCAgEAsLeSrXQC6qkXADn+KmW8F5CgNdQ1hpzHBXZv\n" \
+ "Y8cWq9t6sHF33H2YRYZSRLmREjxiXe+/+988AzaQ60ALpl2YkvJ8OVGvU3Ou5Ya+\n" \
+ "WKMm9ieXaTv+TvfFo/Kb0b0SnPcQ1aJoxeQxwIb09BZDapqxMHLo8ZOtoPUZ441m\n" \
+ "5qQWGVXQ9wyLL+EsaeAOet9GrmNuRnkvIHfmc5rR23uzZwoz/FLoQqM0CXQ4STrA\n" \
+ "PRu0DMEqafNp5VU391UegYFMXspQiUUbDngHzw1cxF0EP3WHzT81ZBcVdTM2Be+x\n" \
+ "HAE9XfjQ7klquspNXK37S/zh15L2iXaMP2oRtzPcNkCCsQFkvkNvVOJlv/kTI755\n" \
+ "Rs29BcTFT0L7ylN7JaS4euMUjTRkChTeYfSNlAqreKUuSGLqNxOYufK7ZZP/+8OY\n" \
+ "/LUs9LHKSCyjjXPb/CKjPkK/b9WiusX3+30+lLe7JgG4aeOInm53ZuIggx8LK4iE\n" \
+ "6B17zIlG1cZmvnFh0lsZR63+QtvwwQ+nAC4Q3EupB40MQJR1i3CZwGsHhiwajteW\n" \
+ "SVKMtx9pPQv/iXfLCH5Boy2rH4Xv2sD3NM4WuzQlwIxJJKcBtlPxvaeiQlemrABf\n" \
+ "FL1lu601jcVdGZTtYRdLVfPh6w5ID1zDzjKHkg4/9FSgqxurC7mdxubsfbMSQtLk\n" \
+ "5tKZN+0=\n" \
+ "-----END CERTIFICATE-----\n"
+
+/**
+ * The private key for #mhdt_ACME_SRV_CERT_PEM
+ */
+#define mhdt_ACME_SRV_KEY_PEM \
+ "-----BEGIN PRIVATE KEY-----\n" \
+ "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\n" \
+ "EJ+F6dRWHnvqXcwIWHjHPPwcsaad4A65\n" \
+ "-----END PRIVATE KEY-----\n"
+
+/**
+ * The ACME challenge certificate for #mhdt_ACME_CHLNG_A_DOMAIN.
+ * RSA 4096, CN=test-acme-challenge
+ */
+#define mhdt_ACME_CHLNG_A_CERT_PEM \
+ "-----BEGIN CERTIFICATE-----\n" \
+ "MIIGGDCCBACgAwIBAgIUWdg2aI2BGpzJpHF3q/3WC3L3p3EwDQYJKoZIhvcNAQEL\n" \
+ "BQAwajELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu\n" \
+ "ZXZhMRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxHDAaBgNVBAMME3Rlc3Qt\n" \
+ "YWNtZS1jaGFsbGVuZ2UwIBcNMjYwODMxMTQzNzIwWhgPMjEyNjA5MDExNDM3MjBa\n" \
+ "MGoxCzAJBgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExDzANBgNVBAcMBkdlbmV2\n" \
+ "YTEbMBkGA1UECgwSdGVzdC1saWJtaWNyb2h0dHBkMRwwGgYDVQQDDBN0ZXN0LWFj\n" \
+ "bWUtY2hhbGxlbmdlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+wAF\n" \
+ "ZGAP0bAc3a6V5BCbddleMgPl7XnCaw8LAvzsnXlReO5gasEV0J4qBkx2b7P8qiJE\n" \
+ "zW/AIAmxat2HlViWPQDcR+lgM9+dCYtlrAE+6/wE8FdSmP5poe0CjqlaC72/1Jbr\n" \
+ "WzkXdC87NbGZEQatGc1Dm59J0T8L60NLSpmEA4MnyxQcyA5bMgDBMkuE2IpIiiY5\n" \
+ "CaSMDvwj0v1o2YjVFni9gpjW557YMJtf5nc3ZHceEHGjF6j5YusBvtqhJ1l9DUN1\n" \
+ "eC3xCMEjuWJP9/z8T6+4oG1mn9cx9rcd/hVvM16BFBZYNj5lAW6D06pLo/NwEbBw\n" \
+ "mlyF5i7tHZejPg7djPjOqely6tlhooMMpSnSZ5Md/v5iIXgNUrM1Rw6fPrIy5Ioo\n" \
+ "GBZ6F+WUb/KsdABy6wXhZIRxb8xXPy3OjI3GRJFHkuZrv7JkV5BrtCx4IbqtaBem\n" \
+ "b6PntExzvtZwnHCm/i+mrpf9COtJwlZI5WXOzZ582OivrA1G9HFdsPH9vYTyzpWH\n" \
+ "9F9Vo7pSKm/pJ6e5Rfxff2zktxwL3nsN3oGhSyelqVnQBSXvOxABMEPVxbbgaKA0\n" \
+ "7eNeHBXwzajeBl1M4gLQlVEDC9li3IpbEfBdzZieSJaNbnd06Jnk+Et7wm646GCV\n" \
+ "DJ2U3Mss2iU18vipCE0JUaT2ecs9gvt9e05X5BcCAwEAAaOBszCBsDAfBgNVHSME\n" \
+ "GDAWgBTpYKJHLroAze4Ii3deRWoldmMm+jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB\n" \
+ "/wQEAwIHgDAdBgNVHREEFjAUghJhY21lLXRlc3QxLmV4YW1wbGUwHQYDVR0OBBYE\n" \
+ "FOlgokcuugDN7giLd15FaiV2Yyb6MDEGCCsGAQUFBwEfAQH/BCIEIPLGBRPQeB8f\n" \
+ "rpo0yvkswvcjBZOCCa4cGnjVp1qMp575MA0GCSqGSIb3DQEBCwUAA4ICAQDkHv8I\n" \
+ "ra7wwYEg/wpjDyZnBAZvZslGyiX1rd58Gwp77htjx6C8ccjwEh05BOwgRFhYDvAs\n" \
+ "Jpi0TXDMN4VI1353UplQQgBWaPbnXuBs8ppPMDRnH5TB+HAvEUJo/xMCwsjzHiub\n" \
+ "Q3uU3xsmY0v01YJT93pWjigmJ3JcsFHBMzRubslsYB2hTu2nSU66KqstLIAKUQ1c\n" \
+ "ih9XtJ+iWE0DwKnlUvc0yiW/G5hmp8LNSOJg6SjTBQWEatSclTvuxS+1Wh1EOXHd\n" \
+ "fIn7Q3YEjbwy3sbwRMYIsRkZ9GFT91FTMV6i5uJ0y2cGrfkZIfvuI53iXbtZh3bt\n" \
+ "EcjVMgHogI9VRwW2zR2UQXDW7Ae6BsriwGsk+gCOfpMyz4qVZ1EN79R6BhxskOHE\n" \
+ "bqkql1G7niIAvX0WnU+4dFrm3Mtw5GY+qv9F0SYZsi7EGB/bhdc2d1vI5Gy2j9v3\n" \
+ "0zPPsEODrbpiMViD3a/9Zg8sGN//iX4IdKXyOuT7wljz40d5RcItgjUwxIIXxnWz\n" \
+ "flx/IaOlezHQygLyp6a4h9Tw+OGv9l+rWEK3OoE5kI2fNXpcCKKMf6C340F0xGUg\n" \
+ "fk/te/o/mxjgNX1pqTehoRuvn7gpMGbaqqPsoJpSdeKolQkOBW5qoZm6i2EXSkVb\n" \
+ "xoi1Eb4j0brypkqwYJqa3n+3Sz2CAS1JEk4eZA==\n" \
+ "-----END CERTIFICATE-----\n"
+
+/**
+ * The private key for #mhdt_ACME_CHLNG_A_CERT_PEM
+ */
+#define mhdt_ACME_CHLNG_A_KEY_PEM \
+ "-----BEGIN PRIVATE KEY-----\n" \
+ "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\n" \
+ "aOxJkk3KdEnf10Zoq7FH9yN/2qxqkXo=\n" \
+ "-----END PRIVATE KEY-----\n"
+
+/**
+ * The ACME challenge certificate for #mhdt_ACME_CHLNG_B_DOMAIN.
+ * ECDSA P-384, CN=test-mhd-server (the same name as the "server" one)
+ */
+#define mhdt_ACME_CHLNG_B_CERT_PEM \
+ "-----BEGIN CERTIFICATE-----\n" \
+ "MIICwTCCAkegAwIBAgIUGvDcAsQcCKB24Bkh8T/9jT1RthgwCgYIKoZIzj0EAwMw\n" \
+ "ZjELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZh\n" \
+ "MRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3QtbWhk\n" \
+ "LXNlcnZlcjAgFw0yNjA4MzExNDM3MjBaGA8yMTI2MDkwMTE0MzcyMFowZjELMAkG\n" \
+ "A1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRswGQYD\n" \
+ "VQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3QtbWhkLXNlcnZl\n" \
+ "cjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOAPQQq1PtnjUgXrgNI8TAwWne0hHb20\n" \
+ "9W7iopT9VnoKJdIXl071kjXjxo3YOPXgn3BC+jQpG/FiJwCCjFtd0WQA2vRAmSpH\n" \
+ "iEXlVx9ZwnxQXfOPyfWOEqglByvQSDKdi6OBszCBsDAfBgNVHSMEGDAWgBTKY8Ch\n" \
+ "2pGmjuS0FmSXU8DbmCYrRzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAd\n" \
+ "BgNVHREEFjAUghJhY21lLXRlc3QyLmV4YW1wbGUwHQYDVR0OBBYEFMpjwKHakaaO\n" \
+ "5LQWZJdTwNuYJitHMDEGCCsGAQUFBwEfAQH/BCIEIIYAoxWrhIsBZAFvbd7j6s/3\n" \
+ "2/DdvCio76/5ndU1HeNZMAoGCCqGSM49BAMDA2gAMGUCMCcJqSeM9Vh47njeifnv\n" \
+ "U97CIsCB7T/yjv9RdhQWer0GS8+yjIk8hkILLFteviY5owIxANeSQclgdhXG/SGP\n" \
+ "dkz2xtjfjvsZMSwJhgA0PQhJ2Ug0WxQcm2vL4w9i9ww21yZIIg==\n" \
+ "-----END CERTIFICATE-----\n"
+
+/**
+ * The private key for #mhdt_ACME_CHLNG_B_CERT_PEM
+ */
+#define mhdt_ACME_CHLNG_B_KEY_PEM \
+ "-----BEGIN PRIVATE KEY-----\n" \
+ "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCH3vJW/8METKqnNfLR\n" \
+ "S9H2QtUKOFiRShg0QnlSIYMky++m9m/4YHnKC4O7r3a0xpehZANiAATgD0EKtT7Z\n" \
+ "41IF64DSPEwMFp3tIR29tPVu4qKU/VZ6CiXSF5dO9ZI148aN2Dj14J9wQvo0KRvx\n" \
+ "YicAgoxbXdFkANr0QJkqR4hF5VcfWcJ8UF3zj8n1jhKoJQcr0EgynYs=\n" \
+ "-----END PRIVATE KEY-----\n"
+
+/* *INDENT-ON* */
+
+#endif /* ! MHD_ACME_TEST_CERTS_H */
diff --git a/src/tests/acme/test_acme_alpn.c b/src/tests/acme/test_acme_alpn.c
@@ -0,0 +1,1674 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
+/*
+ This file is part of GNU libmicrohttpd.
+ Copyright (C) 2026 Evgeny Grin (Karlson2k)
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ Alternatively, you can redistribute GNU libmicrohttpd and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of
+ the License, or (at your option) any later version, together
+ with the eCos exception, as follows:
+
+ As a special exception, if other files instantiate templates or
+ use macros or inline functions from this file, or you compile this
+ file and link it with other works to produce a work based on this
+ file, this file does not by itself cause the resulting work to be
+ covered by the GNU General Public License. However the source code
+ for this file must still be made available in accordance with
+ section (3) of the GNU General Public License v2.
+
+ This exception does not invalidate any other reasons why a work
+ based on this file might be covered by the GNU General Public
+ License.
+
+ You should have received copies of the GNU Lesser General Public
+ License and the GNU General Public License along with this library;
+ if not, see <https://www.gnu.org/licenses/>.
+*/
+
+/**
+ * @file src/tests/acme/test_acme_alpn.c
+ * @brief Test for the ACME TLS-ALPN-01 challenge support
+ * @author Karlson2k (Evgeny Grin)
+ *
+ * The test starts the daemon with two ACME challenge certificates and
+ * connects to it with a simple GnuTLS client. For each tested combination of
+ * the SNI and the offered ALPN protocols the test checks which certificate has
+ * been served and which application protocol has been negotiated.
+ */
+
+#include "mhd_sys_options.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include "sys_bool_type.h"
+
+#include "sys_sockets_headers.h"
+#include "sys_sockets_types.h"
+#include "sys_ip_headers.h"
+#include "mhd_sockets_macros.h"
+#include <fcntl.h>
+
+#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
+
+#include "microhttpd2.h"
+
+#include "acme_test_certs.h"
+
+
+/* The ACME TLS-ALPN-01 challenge requires ALPN, which is supported by
+ GnuTLS 3.2.0 and later. The test client uses the same library. */
+#if GNUTLS_VERSION_NUMBER >= 0x030200
+
+# ifndef mhd_SSTR_LEN
+/**
+ * Determine the length of the static string at compile time
+ */
+# define mhd_SSTR_LEN(sstr) (sizeof(sstr) / sizeof(char) - 1)
+# endif /* ! mhd_SSTR_LEN */
+
+
+# if defined(MHD_SOCKETS_KIND_POSIX)
+# if defined(ENETUNREACH)
+# define mhdt_SCKT_HARD_ERR ENETUNREACH
+# elif defined(ENOTCONN)
+# define mhdt_SCKT_HARD_ERR ENOTCONN
+# elif defined(ECONNRESET)
+# define mhdt_SCKT_HARD_ERR ECONNRESET
+# elif defined(EPIPE)
+# define mhdt_SCKT_HARD_ERR EPIPE
+# elif defined(EBADF)
+# define mhdt_SCKT_HARD_ERR EBADF
+# else
+# define mhdt_SCKT_HARD_ERR 99 /* Fallback, never used in practice */
+# endif
+# else /* MHD_SOCKETS_KIND_WINSOCK */
+# define mhdt_SCKT_HARD_ERR WSAENETRESET
+# endif
+
+
+MHD_NORETURN_ static void
+_externalErrorExit_func (const char *errDesc,
+ const char *funcName,
+ int lineNum)
+{
+ int last_errno = errno;
+
+ fflush (stdout);
+ if ((NULL != errDesc) && (0 != errDesc[0]))
+ fprintf (stderr, "!!! %s", errDesc);
+ else
+ fprintf (stderr, "!!! System or external library call failed");
+ if ((NULL != funcName) && (0 != funcName[0]))
+ fprintf (stderr, " in %s", funcName);
+ if (0 < lineNum)
+ fprintf (stderr, " at line %d", lineNum);
+
+ fprintf (stderr, ".\nLast errno value: %d (%s)\n", last_errno,
+ strerror (last_errno));
+# ifdef MHD_SOCKETS_KIND_WINSOCK
+ fprintf (stderr, "WSAGetLastError() value: %d\n", (int)WSAGetLastError ());
+# endif /* MHD_SOCKETS_KIND_WINSOCK */
+ fflush (stderr);
+ exit (99);
+}
+
+
+MHD_NORETURN_ static void
+_mhdErrorExit_func (const char *errDesc,
+ const char *funcName,
+ int lineNum)
+{
+ int last_errno = errno;
+
+ fflush (stdout);
+ if ((NULL != errDesc) && (0 != errDesc[0]))
+ fprintf (stderr, "!!! %s", errDesc);
+ else
+ fprintf (stderr, "!!! MHD unexpected error");
+ if ((NULL != funcName) && (0 != funcName[0]))
+ fprintf (stderr, " in %s", funcName);
+ if (0 < lineNum)
+ fprintf (stderr, " at line %d", lineNum);
+
+ fprintf (stderr, ".\nLast errno value: %d (%s)\n", last_errno,
+ strerror (last_errno));
+ fflush (stderr);
+ exit (8);
+}
+
+
+static void
+_testErrorLog_func (const char *errDesc,
+ const char *funcName,
+ int lineNum)
+{
+ int last_errno = errno;
+
+ fflush (stdout);
+ if ((NULL != errDesc) && (0 != errDesc[0]))
+ fprintf (stderr, "!!! %s", errDesc);
+ else
+ fprintf (stderr, "!!! System or external library call resulted in error");
+ if ((NULL != funcName) && (0 != funcName[0]))
+ fprintf (stderr, " in %s", funcName);
+ if (0 < lineNum)
+ fprintf (stderr, " at line %d", lineNum);
+
+ fprintf (stderr, ".\nLast errno value: %d (%s)\n", last_errno,
+ strerror (last_errno));
+# ifdef MHD_SOCKETS_KIND_WINSOCK
+ fprintf (stderr, "WSAGetLastError() value: %d\n", (int)WSAGetLastError ());
+# endif /* MHD_SOCKETS_KIND_WINSOCK */
+ fflush (stderr);
+}
+
+
+# ifdef MHD_HAVE_MHD_FUNC_
+# define externalErrorExitDesc(errDesc) \
+ _externalErrorExit_func (errDesc, MHD_FUNC_, __LINE__)
+# define mhdErrorExitDesc(errDesc) \
+ _mhdErrorExit_func (errDesc, MHD_FUNC_, __LINE__)
+# define testErrorLogDesc(errDesc) \
+ _testErrorLog_func (errDesc, MHD_FUNC_, __LINE__)
+# else /* ! MHD_HAVE_MHD_FUNC_ */
+# define externalErrorExitDesc(errDesc) \
+ _externalErrorExit_func (errDesc, NULL, __LINE__)
+# define mhdErrorExitDesc(errDesc) \
+ _mhdErrorExit_func (errDesc, NULL, __LINE__)
+# define testErrorLogDesc(errDesc) \
+ _testErrorLog_func (errDesc, NULL, __LINE__)
+# endif /* ! MHD_HAVE_MHD_FUNC_ */
+
+
+/* Could be increased to facilitate debugging */
+static int test_timeout = 5;
+
+
+/**
+ * The string with the length known at compile time
+ */
+struct TestStr
+{
+ /**
+ * The pointer to the string, NULL if the string is not used
+ */
+ const char *str;
+ /**
+ * The length of the @a str, not including the terminating null character
+ */
+ size_t len;
+};
+
+/**
+ * Static initialiser for #TestStr from the static string
+ */
+# define TSTR(s) { (s), mhd_SSTR_LEN (s) }
+
+/**
+ * Static initialiser for the unused #TestStr
+ */
+# define TSTR_NONE { NULL, 0 }
+
+
+/**
+ * The ALPN protocol name for the ACME TLS-ALPN-01 challenge
+ */
+# define ALPN_ACME "acme-tls/1"
+
+/**
+ * The ALPN protocol name for HTTP/1.1
+ */
+# define ALPN_HTTP_1_1 "http/1.1"
+
+/**
+ * The domain without any ACME challenge certificate
+ */
+# define DOMAIN_NO_ACME "acme-test3.example"
+
+/**
+ * The first ACME domain spelled in the different letters case
+ */
+# define DOMAIN_A_MIXED_CASE "ACME-Test1.Example"
+
+/**
+ * The domain used only for the calls that must be rejected
+ */
+# define DOMAIN_BAD_CERT "bad-cert.example"
+
+/**
+ * The data that looks like a certificate, but is not one
+ */
+# define BAD_CERT_PEM \
+ "-----BEGIN CERTIFICATE-----\n" \
+ "this is not a certificate\n" \
+ "-----END CERTIFICATE-----\n"
+
+/**
+ * The maximum number of the ALPN protocols offered by the test client
+ */
+# define MAX_ALPN_PROTS 2
+
+
+/**
+ * The certificate expected from the daemon
+ */
+enum ExpectedCert
+{
+ /**
+ * The certificate served for all non-ACME connections
+ */
+ CERT_SERVER = 0
+ ,
+ /**
+ * The ACME challenge certificate for #mhdt_ACME_CHLNG_A_DOMAIN
+ */
+ CERT_CHLNG_A
+ ,
+ /**
+ * The ACME challenge certificate for #mhdt_ACME_CHLNG_B_DOMAIN
+ */
+ CERT_CHLNG_B
+};
+
+/**
+ * Set to 'true' if the daemon has received any request.
+ *
+ * The test client never sends a request, so this must not happen.
+ */
+static volatile bool unexpected_request;
+
+
+/**
+ * The certificates in PEM format, indexed by #ExpectedCert
+ */
+static const struct TestStr certs_pem[] = {
+ TSTR (mhdt_ACME_SRV_CERT_PEM),
+ TSTR (mhdt_ACME_CHLNG_A_CERT_PEM),
+ TSTR (mhdt_ACME_CHLNG_B_CERT_PEM)
+};
+
+/**
+ * The names of the certificates for the log, indexed by #ExpectedCert
+ */
+static const char *const certs_name[] = {
+ "the server certificate",
+ "the challenge certificate \"A\"",
+ "the challenge certificate \"B\""
+};
+
+
+/**
+ * The parameters and the expected results for a single connection
+ */
+struct TestCase
+{
+ /**
+ * The name of the test case
+ */
+ const char *label;
+ /**
+ * The domain name sent as SNI, unused string to send no SNI
+ */
+ struct TestStr sni;
+ /**
+ * The ALPN protocols offered by the client, terminated by the unused string
+ */
+ struct TestStr alpn[MAX_ALPN_PROTS + 1];
+ /**
+ * The protocol expected to be negotiated, unused string if none must be
+ * selected
+ */
+ struct TestStr expect_alpn;
+ /**
+ * The certificate expected to be served
+ */
+ enum ExpectedCert expect_cert;
+};
+
+
+/**
+ * The test cases performed for every TLS backend
+ */
+static const struct TestCase test_cases[] = {
+ {
+ "the ACME challenge for the first domain",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_ACME),
+ CERT_CHLNG_A
+ },
+ {
+ "the ACME challenge for the second domain",
+ TSTR (mhdt_ACME_CHLNG_B_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_ACME),
+ CERT_CHLNG_B
+ },
+ {
+ "the ACME domain in the different letters case",
+ TSTR (DOMAIN_A_MIXED_CASE),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_ACME),
+ CERT_CHLNG_A
+ },
+ {
+ "the ACME protocol together with the HTTP protocol",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR (ALPN_HTTP_1_1), TSTR_NONE },
+ TSTR (ALPN_HTTP_1_1),
+ CERT_SERVER
+ },
+ {
+ "the ACME protocol for the domain without the challenge",
+ TSTR (DOMAIN_NO_ACME),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+ },
+ {
+ "the ACME protocol without the domain name",
+ TSTR_NONE,
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+ },
+ {
+ "the HTTP protocol for the ACME domain",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_HTTP_1_1), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_HTTP_1_1),
+ CERT_SERVER
+ },
+ {
+ "the ACME domain without the ALPN extension",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR_NONE, TSTR_NONE, TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+ },
+ {
+ "the HTTP protocol offered before the ACME protocol",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_HTTP_1_1), TSTR (ALPN_ACME), TSTR_NONE },
+ TSTR (ALPN_HTTP_1_1),
+ CERT_SERVER
+ },
+ {
+ "the ACME protocol offered twice",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR (ALPN_ACME), TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+ }
+};
+
+/**
+ * The test case performed after the removal of the challenge certificate
+ * for the first domain
+ */
+static const struct TestCase removed_case = {
+ "the certificate removed by the differently spelled domain is not served",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+};
+
+/**
+ * The check for the fallback certificate: the domain without a certificate
+ * of its own is served with the certificate registered for any domain
+ */
+static const struct TestCase fallback_case = {
+ "the fallback certificate is served for the unknown domain",
+ TSTR (DOMAIN_NO_ACME),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_ACME),
+ CERT_CHLNG_A
+};
+
+/**
+ * The check that the fallback certificate is not used without the SNI
+ */
+static const struct TestCase fallback_no_sni_case = {
+ "the fallback certificate is not served without the domain name",
+ TSTR_NONE,
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+};
+
+/**
+ * The check that the certificate of the domain takes precedence over
+ * the fallback certificate
+ */
+static const struct TestCase fallback_specific_case = {
+ "the domain certificate takes precedence over the fallback certificate",
+ TSTR (mhdt_ACME_CHLNG_B_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_ACME),
+ CERT_CHLNG_B
+};
+
+/**
+ * The check performed after the challenge certificate for the first domain
+ * has been replaced by another certificate
+ */
+static const struct TestCase replaced_case = {
+ "the replaced challenge certificate is served",
+ TSTR (mhdt_ACME_CHLNG_A_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR (ALPN_ACME),
+ CERT_CHLNG_B
+};
+
+/**
+ * The check performed after the removal of all the challenge certificates
+ * by the single call. The second domain is used: its certificate has never
+ * been removed individually.
+ */
+static const struct TestCase cleared_case = {
+ "no challenge certificate is served after the removal of all of them",
+ TSTR (mhdt_ACME_CHLNG_B_DOMAIN),
+ { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE },
+ TSTR_NONE,
+ CERT_SERVER
+};
+
+
+/**
+ * Set the socket to the non-blocking mode
+ * @param fd the socket to set the mode for
+ */
+static void
+make_nonblocking (MHD_Socket fd)
+{
+# if defined(MHD_SOCKETS_KIND_POSIX)
+ int flags;
+
+ flags = fcntl (fd, F_GETFL);
+ if (-1 == flags)
+ externalErrorExitDesc ("fcntl() failed");
+ if (O_NONBLOCK != (flags & O_NONBLOCK))
+ if (-1 == fcntl (fd, F_SETFL, flags | O_NONBLOCK))
+ externalErrorExitDesc ("fcntl() failed");
+# elif defined(MHD_SOCKETS_KIND_WINSOCK)
+ unsigned long flags = 1;
+
+ if (0 != ioctlsocket (fd, (int)FIONBIO, &flags))
+ externalErrorExitDesc ("ioctlsocket() failed");
+# endif /* MHD_SOCKETS_KIND_WINSOCK */
+}
+
+
+/**
+ * Disable Nagle's algorithm on the socket, the failure is not fatal
+ * @param fd the socket to set the option for
+ */
+static void
+make_nodelay (MHD_Socket fd)
+{
+# ifdef TCP_NODELAY
+ const mhd_SCKT_OPT_BOOL on_val = 1;
+
+ if (0 == setsockopt (fd,
+ IPPROTO_TCP,
+ TCP_NODELAY,
+ (const void *)&on_val,
+ sizeof (on_val)))
+ return; /* Success exit point */
+
+# ifndef MHD_SOCKETS_KIND_WINSOCK
+ fprintf (stderr, "Failed to enable TCP_NODELAY on socket (ignored). "
+ "errno: %d (%s)\n", (int)errno, strerror (errno));
+# else /* MHD_SOCKETS_KIND_WINSOCK */
+ fprintf (stderr, "Failed to enable TCP_NODELAY on socket (ignored). "
+ "WSAGetLastError() value: %d\n", (int)WSAGetLastError ());
+# endif /* MHD_SOCKETS_KIND_WINSOCK */
+ fflush (stderr);
+# else /* ! TCP_NODELAY */
+ (void)fd; /* Unused */
+# endif /* ! TCP_NODELAY */
+}
+
+
+/**
+ * The client socket with the TLS session on it
+ */
+struct wr_socket
+{
+ /**
+ * The socket
+ */
+ MHD_Socket fd;
+ /**
+ * 'true' if the socket is in the non-blocking mode
+ */
+ bool is_nonblocking;
+ /**
+ * The TLS credentials, without any trusted CA
+ */
+ gnutls_certificate_credentials_t tls_crd;
+ /**
+ * The TLS session
+ */
+ gnutls_session_t tls_s;
+ /**
+ * 'true' if the TLS handshake has been completed
+ */
+ bool tls_connected;
+};
+
+
+/**
+ * Create the socket with the TLS session, without connecting it.
+ *
+ * The server certificate is not verified: the daemon uses self-signed
+ * certificates.
+ *
+ * @return the created socket,
+ * NULL on failure
+ */
+static struct wr_socket *
+wr_create_tls_sckt (void)
+{
+ struct wr_socket *s;
+
+ s = (struct wr_socket *)malloc (sizeof(struct wr_socket));
+ if (NULL == s)
+ {
+ testErrorLogDesc ("malloc() failed");
+ return NULL;
+ }
+ s->is_nonblocking = false;
+ s->tls_connected = false;
+ s->fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (MHD_INVALID_SOCKET != s->fd)
+ {
+ make_nodelay (s->fd);
+ if (GNUTLS_E_SUCCESS == gnutls_init (&(s->tls_s), GNUTLS_CLIENT))
+ {
+ if (GNUTLS_E_SUCCESS == gnutls_set_default_priority (s->tls_s))
+ {
+ if (GNUTLS_E_SUCCESS ==
+ gnutls_certificate_allocate_credentials (&(s->tls_crd)))
+ {
+ if (GNUTLS_E_SUCCESS ==
+ gnutls_credentials_set (s->tls_s,
+ GNUTLS_CRD_CERTIFICATE,
+ s->tls_crd))
+ {
+# ifndef _WIN64
+ gnutls_transport_set_int (s->tls_s, (int)(s->fd));
+# else /* Win x64 */
+ /* The socket does not fit into "int" */
+ gnutls_transport_set_ptr (s->tls_s,
+ (gnutls_transport_ptr_t) \
+ (intptr_t)(s->fd));
+# endif /* Win x64 */
+ return s; /* Success exit point */
+ }
+ else
+ testErrorLogDesc ("gnutls_credentials_set() failed");
+
+ /* Below is a clean-up path */
+ gnutls_certificate_free_credentials (s->tls_crd);
+ }
+ else
+ testErrorLogDesc ("gnutls_certificate_allocate_credentials() failed");
+ }
+ else
+ testErrorLogDesc ("gnutls_set_default_priority() failed");
+ gnutls_deinit (s->tls_s);
+ }
+ else
+ testErrorLogDesc ("gnutls_init() failed");
+ (void)mhd_socket_close (s->fd);
+ }
+ else
+ testErrorLogDesc ("socket() failed");
+ free (s);
+ return NULL; /* Failure exit point */
+}
+
+
+/**
+ * Close the socket and release the TLS session
+ * @param s the socket to close
+ */
+static void
+wr_close (struct wr_socket *s)
+{
+ (void)mhd_socket_close (s->fd);
+ gnutls_deinit (s->tls_s);
+ gnutls_certificate_free_credentials (s->tls_crd);
+ free (s);
+}
+
+
+/**
+ * Set the socket to the non-blocking mode if it is not set already
+ * @param s the socket to set the mode for
+ */
+static void
+wr_make_nonblocking (struct wr_socket *s)
+{
+ if (!s->is_nonblocking)
+ make_nonblocking (s->fd);
+ s->is_nonblocking = true;
+}
+
+
+/**
+ * The direction to wait for
+ */
+enum wr_wait_for_type
+{
+ WR_WAIT_FOR_RECV = 0
+ ,
+ WR_WAIT_FOR_SEND = 1
+};
+
+
+/**
+ * Wait for the socket to become ready for the specified operation.
+ *
+ * @param s the socket to wait for
+ * @param timeout_ms the maximum wait time in milliseconds,
+ * no limit if negative value is used
+ * @param wait_for the direction to wait for
+ * @return 'true' if the socket is ready,
+ * 'false' on timeout or error
+ */
+static bool
+wr_wait_socket_ready_noabort_ (struct wr_socket *s,
+ int timeout_ms,
+ enum wr_wait_for_type wait_for)
+{
+ fd_set fds;
+ int sel_res;
+ struct timeval tmo;
+ struct timeval *tmo_ptr;
+
+# ifndef MHD_SOCKETS_KIND_WINSOCK
+ if (FD_SETSIZE <= s->fd)
+ externalErrorExitDesc ("Too large FD value");
+# endif /* ! MHD_SOCKETS_KIND_WINSOCK */
+ FD_ZERO (&fds);
+ FD_SET (s->fd, &fds);
+ if (0 <= timeout_ms)
+ {
+# if !defined(_WIN32) || defined(__CYGWIN__)
+ tmo.tv_sec = (time_t)(timeout_ms / 1000);
+# else /* Native W32 */
+ tmo.tv_sec = (long)(timeout_ms / 1000);
+# endif /* Native W32 */
+ tmo.tv_usec = ((long)(timeout_ms % 1000)) * 1000;
+ tmo_ptr = &tmo;
+ }
+ else
+ tmo_ptr = NULL; /* No timeout */
+
+ do
+ {
+ if (WR_WAIT_FOR_RECV == wait_for)
+ sel_res = select (1 + (int)s->fd, &fds, NULL, NULL, tmo_ptr);
+ else
+ sel_res = select (1 + (int)s->fd, NULL, &fds, NULL, tmo_ptr);
+ } while ((0 > sel_res) && mhd_SCKT_ERR_IS_EINTR (mhd_SCKT_GET_LERR ()));
+
+ if (1 == sel_res)
+ return true;
+
+ if (0 == sel_res)
+ fprintf (stderr, "Timeout");
+ else
+ {
+# ifndef MHD_SOCKETS_KIND_WINSOCK
+ fprintf (stderr, "Error %d (%s)", (int)errno, strerror (errno));
+# else /* MHD_SOCKETS_KIND_WINSOCK */
+ fprintf (stderr, "Error (WSAGetLastError code: %d)",
+ (int)WSAGetLastError ());
+# endif /* MHD_SOCKETS_KIND_WINSOCK */
+ }
+ fprintf (stderr, " waiting for socket to be available for %s.\n",
+ (WR_WAIT_FOR_RECV == wait_for) ? "receiving" : "sending");
+ return false;
+}
+
+
+/**
+ * Wait for the socket to become ready, abort the test on failure
+ * @param s the socket to wait for
+ * @param timeout_ms the maximum wait time in milliseconds
+ * @param wait_for the direction to wait for
+ */
+static void
+wr_wait_socket_ready_ (struct wr_socket *s,
+ int timeout_ms,
+ enum wr_wait_for_type wait_for)
+{
+ if (wr_wait_socket_ready_noabort_ (s, timeout_ms, wait_for))
+ return;
+
+ if (WR_WAIT_FOR_RECV == wait_for)
+ mhdErrorExitDesc ("The daemon did not send the data in time");
+ else
+ mhdErrorExitDesc ("The daemon did not accept the data in time");
+}
+
+
+/**
+ * Connect the socket to the specified address.
+ *
+ * @param s the socket to connect
+ * @param addr the address to connect to
+ * @param length the size of the structure pointed by @a addr
+ * @param timeout_ms the maximum wait time in milliseconds
+ * @return 'true' on success,
+ * 'false' otherwise
+ */
+static bool
+wr_connect_tmo (struct wr_socket *s,
+ const struct sockaddr *addr,
+ unsigned int length,
+ int timeout_ms)
+{
+ if (0 != connect (s->fd, addr, (socklen_t)length))
+ {
+ int err;
+ bool connect_completed = false;
+
+ err = mhd_SCKT_GET_LERR ();
+# if defined(MHD_SOCKETS_KIND_POSIX)
+ while ((!connect_completed) && (EINTR == err))
+ {
+ connect_completed = (0 == connect (s->fd, addr, (socklen_t)length));
+ if (!connect_completed)
+ {
+ err = errno;
+ if (EALREADY == err)
+ err = EINPROGRESS;
+ else if (EISCONN == err)
+ connect_completed = true;
+ }
+ }
+# endif /* MHD_SOCKETS_KIND_POSIX */
+ /* No modern system uses EAGAIN here, except W32 */
+ if ((!connect_completed)
+ && (mhd_SCKT_ERR_IS_INPROGRESS (err)
+ || mhd_SCKT_ERR_IS_EAGAIN (err)))
+ connect_completed =
+ wr_wait_socket_ready_noabort_ (s, timeout_ms, WR_WAIT_FOR_SEND);
+ if (!connect_completed)
+ {
+ testErrorLogDesc ("connect() failed");
+ return false;
+ }
+ }
+ return true;
+}
+
+
+/**
+ * Connect the socket to the daemon on the loopback interface
+ * @param s the socket to connect
+ * @param port the port of the daemon
+ * @return 'true' on success,
+ * 'false' otherwise
+ */
+static bool
+wr_connect_to_daemon (struct wr_socket *s,
+ uint16_t port)
+{
+ struct sockaddr_in sa;
+
+ memset (&sa, 0, sizeof(sa));
+ sa.sin_family = AF_INET;
+ sa.sin_port = htons (port);
+ if (1 != inet_pton (AF_INET,
+ "127.0.0.1",
+ &(sa.sin_addr)))
+ {
+ testErrorLogDesc ("inet_pton() failed");
+ return false;
+ }
+ return wr_connect_tmo (s,
+ (const struct sockaddr *)&sa,
+ (unsigned int)sizeof(sa),
+ test_timeout * 1000);
+}
+
+
+/**
+ * The result of the TLS handshake performed by the test client
+ */
+enum HandshakeResult
+{
+ /**
+ * The handshake has been completed
+ */
+ HSK_COMPLETED = 0
+ ,
+ /**
+ * The daemon aborted the handshake as no offered ALPN protocol matched
+ */
+ HSK_NO_ALPN_ALERT
+ ,
+ /**
+ * The handshake failed
+ */
+ HSK_FAILED
+};
+
+
+/**
+ * Perform the TLS handshake.
+ *
+ * Unlike the same function in the "upgrade" test, the rejection by the
+ * "no_application_protocol" alert is reported separately: it is the expected
+ * result for some of the test cases.
+ *
+ * @param s the socket to perform the handshake on
+ * @param timeout_ms the maximum wait time in milliseconds
+ * @return the result of the handshake
+ */
+static enum HandshakeResult
+wr_handshake_tmo_ (struct wr_socket *s,
+ int timeout_ms)
+{
+ int res;
+
+ res = gnutls_handshake (s->tls_s);
+ while ((GNUTLS_E_AGAIN == res) || (GNUTLS_E_INTERRUPTED == res))
+ {
+ wr_wait_socket_ready_ (s, timeout_ms,
+ gnutls_record_get_direction (s->tls_s) ?
+ WR_WAIT_FOR_SEND : WR_WAIT_FOR_RECV);
+ res = gnutls_handshake (s->tls_s);
+ }
+ if (GNUTLS_E_SUCCESS == res)
+ {
+ s->tls_connected = true;
+ return HSK_COMPLETED;
+ }
+ if ((GNUTLS_E_FATAL_ALERT_RECEIVED == res)
+ && (GNUTLS_A_NO_APPLICATION_PROTOCOL == gnutls_alert_get (s->tls_s)))
+ return HSK_NO_ALPN_ALERT;
+
+ fprintf (stderr, "The error returned by gnutls_handshake() is "
+ "'%s' ", gnutls_strerror (res));
+ fprintf (stderr, "(%s)\n", gnutls_strerror_name (res));
+ testErrorLogDesc ("gnutls_handshake() failed with hard error");
+ mhd_SCKT_SET_LERR (mhdt_SCKT_HARD_ERR); /* hard error */
+ return HSK_FAILED;
+}
+
+
+/**
+ * Perform the TLS handshake with the default timeout
+ * @param s the socket to perform the handshake on
+ * @return the result of the handshake
+ */
+static enum HandshakeResult
+wr_handshake (struct wr_socket *s)
+{
+ return wr_handshake_tmo_ (s, test_timeout * 1000);
+}
+
+
+/**
+ * Send the TLS "close notify" alert, the failure is not fatal.
+ *
+ * The daemon should not be left with the abruptly terminated connection.
+ *
+ * @param s the socket to close the session on
+ * @param timeout_ms the maximum wait time in milliseconds
+ */
+static void
+wr_close_notify_tmo_ (struct wr_socket *s,
+ int timeout_ms)
+{
+ int res;
+
+ if (!s->tls_connected)
+ return;
+
+ res = gnutls_bye (s->tls_s, GNUTLS_SHUT_WR);
+ while ((GNUTLS_E_AGAIN == res) || (GNUTLS_E_INTERRUPTED == res))
+ {
+ if (!wr_wait_socket_ready_noabort_ (s, timeout_ms,
+ gnutls_record_get_direction (s->tls_s) ?
+ WR_WAIT_FOR_SEND : WR_WAIT_FOR_RECV))
+ return;
+ res = gnutls_bye (s->tls_s, GNUTLS_SHUT_WR);
+ }
+}
+
+
+/**
+ * Set the SNI and the ALPN protocols requested by the test case.
+ *
+ * @param s the socket to set the extensions for
+ * @param tc the test case being performed
+ * @return 'true' on success,
+ * 'false' otherwise
+ */
+static bool
+client_set_extensions (struct wr_socket *s,
+ const struct TestCase *tc)
+{
+ gnutls_datum_t prots[MAX_ALPN_PROTS];
+ unsigned int num_prots;
+
+ if (NULL != tc->sni.str)
+ {
+ if (GNUTLS_E_SUCCESS !=
+ gnutls_server_name_set (s->tls_s,
+ GNUTLS_NAME_DNS,
+ tc->sni.str,
+ tc->sni.len))
+ {
+ testErrorLogDesc ("gnutls_server_name_set() failed");
+ return false;
+ }
+ }
+ for (num_prots = 0; NULL != tc->alpn[num_prots].str; ++num_prots)
+ {
+ prots[num_prots].data =
+ (unsigned char *)mhd_DROP_CONST (tc->alpn[num_prots].str);
+ prots[num_prots].size = (unsigned int)tc->alpn[num_prots].len;
+ }
+ if (0 == num_prots)
+ return true; /* Send no ALPN extension at all */
+
+ if (GNUTLS_E_SUCCESS !=
+ gnutls_alpn_set_protocols (s->tls_s,
+ prots,
+ num_prots,
+ 0))
+ {
+ testErrorLogDesc ("gnutls_alpn_set_protocols() failed");
+ return false;
+ }
+ return true;
+}
+
+
+/**
+ * Check that the daemon has served the expected certificate.
+ *
+ * The certificates are compared in the DER form: the challenge certificate
+ * "B" repeats the subject of the server certificate, so the names alone
+ * cannot tell them apart.
+ *
+ * @param sess the completed session
+ * @param tc the test case being performed
+ * @return 'true' if the expected certificate has been served,
+ * 'false' otherwise
+ */
+static bool
+check_served_cert (gnutls_session_t sess,
+ const struct TestCase *tc)
+{
+ const gnutls_datum_t *peers;
+ unsigned int num_peers;
+ gnutls_x509_crt_t crt;
+ gnutls_datum_t pem;
+ gnutls_datum_t der;
+ bool res;
+
+ peers = gnutls_certificate_get_peers (sess,
+ &num_peers);
+ if ((NULL == peers) || (0 == num_peers))
+ {
+ fprintf (stderr,
+ "The daemon has not sent any certificate.\n");
+ return false;
+ }
+ if (GNUTLS_E_SUCCESS != gnutls_x509_crt_init (&crt))
+ {
+ fprintf (stderr,
+ "gnutls_x509_crt_init() failed.\n");
+ return false;
+ }
+ pem.data =
+ (unsigned char *)mhd_DROP_CONST (certs_pem[tc->expect_cert].str);
+ pem.size = (unsigned int)certs_pem[tc->expect_cert].len;
+ res = false;
+ if (GNUTLS_E_SUCCESS == gnutls_x509_crt_import (crt,
+ &pem,
+ GNUTLS_X509_FMT_PEM))
+ {
+ if (GNUTLS_E_SUCCESS == gnutls_x509_crt_export2 (crt,
+ GNUTLS_X509_FMT_DER,
+ &der))
+ {
+ res = ((der.size == peers[0].size)
+ && (0 == memcmp (der.data,
+ peers[0].data,
+ (size_t)der.size)));
+ gnutls_free (der.data);
+ }
+ else
+ fprintf (stderr,
+ "gnutls_x509_crt_export2() failed.\n");
+ }
+ else
+ fprintf (stderr,
+ "gnutls_x509_crt_import() failed.\n");
+ gnutls_x509_crt_deinit (crt);
+ if (!res)
+ fprintf (stderr,
+ "The daemon has not served %s.\n",
+ certs_name[tc->expect_cert]);
+ return res;
+}
+
+
+/**
+ * Check that the expected application protocol has been negotiated.
+ *
+ * @param sess the completed session
+ * @param tc the test case being performed
+ * @return 'true' if the result matches the expectation,
+ * 'false' otherwise
+ */
+static bool
+check_selected_alpn (gnutls_session_t sess,
+ const struct TestCase *tc)
+{
+ gnutls_datum_t alpn;
+
+ if (GNUTLS_E_SUCCESS != gnutls_alpn_get_selected_protocol (sess,
+ &alpn))
+ {
+ if (NULL == tc->expect_alpn.str)
+ return true;
+ fprintf (stderr,
+ "No application protocol has been negotiated, \"%s\" expected.\n",
+ tc->expect_alpn.str);
+ return false;
+ }
+ if ((NULL != tc->expect_alpn.str)
+ && (tc->expect_alpn.len == (size_t)alpn.size)
+ && (0 == memcmp (tc->expect_alpn.str,
+ alpn.data,
+ (size_t)alpn.size)))
+ return true;
+
+ fprintf (stderr,
+ "The negotiated application protocol is \"%.*s\", %s expected.\n",
+ (int)alpn.size,
+ (const char *)alpn.data,
+ (NULL != tc->expect_alpn.str) ? tc->expect_alpn.str : "none");
+ return false;
+}
+
+
+/**
+ * Perform a single test case.
+ *
+ * @param port the port of the daemon
+ * @param tc the test case to perform
+ * @return 'true' if the results are as expected,
+ * 'false' otherwise
+ */
+static bool
+run_test_case (uint16_t port,
+ const struct TestCase *tc)
+{
+ struct wr_socket *s;
+ bool res;
+
+ s = wr_create_tls_sckt ();
+ if (NULL == s)
+ return false;
+
+ if (client_set_extensions (s,
+ tc))
+ {
+ wr_make_nonblocking (s);
+ if (wr_connect_to_daemon (s,
+ port))
+ {
+ switch (wr_handshake (s))
+ {
+ case HSK_COMPLETED:
+ res = check_selected_alpn (s->tls_s,
+ tc);
+ if (!check_served_cert (s->tls_s,
+ tc))
+ res = false;
+ break;
+ case HSK_NO_ALPN_ALERT:
+ /* The daemon supports none of the offered protocols and aborts the
+ handshake, as required by RFC 7301, section 3.2. The certificate
+ cannot be checked, the test case is passed if no protocol was
+ expected to be negotiated. */
+ res = (NULL == tc->expect_alpn.str);
+ if (!res)
+ fprintf (stderr,
+ "The daemon rejected the offered application protocols.\n");
+ break;
+ case HSK_FAILED:
+ default:
+ res = false;
+ break;
+ }
+ wr_close_notify_tmo_ (s,
+ test_timeout * 1000);
+ }
+ else
+ res = false;
+ }
+ else
+ res = false;
+ wr_close (s);
+ return res;
+}
+
+
+/**
+ * Check the status code returned by the rejected call
+ *
+ * @param sc the returned status code
+ * @param expected the expected status code
+ * @param descr the description of the checked call
+ * @return 'true' if the code is the expected one,
+ * 'false' otherwise
+ */
+static bool
+check_status (enum MHD_StatusCode sc,
+ enum MHD_StatusCode expected,
+ const char *descr)
+{
+ if (expected == sc)
+ return true;
+
+ fprintf (stderr,
+ "The status code for %s is %u, %u expected.\n",
+ descr,
+ (unsigned int)sc,
+ (unsigned int)expected);
+ return false;
+}
+
+
+/**
+ * The daemon request callback.
+ *
+ * The test client never sends any request, the callback is required by
+ * #MHD_daemon_create().
+ */
+static const struct MHD_Action *
+server_req_cb (void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ (void)cls; /* Unused */
+ (void)path; /* Unused */
+ (void)method; /* Unused */
+ (void)upload_size; /* Unused */
+
+ unexpected_request = true;
+ fprintf (stderr,
+ "Unexpected request received.\n");
+ return MHD_action_from_response (
+ request,
+ MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT));
+}
+
+
+/**
+ * Configure the started daemon and add the ACME challenge certificates.
+ *
+ * Everything set here is owned by the daemon, so nothing has to be released
+ * by this function on failure.
+ *
+ * @param d the created daemon
+ * @param be the TLS backend to use
+ * @param[out] p_port the port bound by the daemon, set on success only
+ * @return 0 on success,
+ * 77 if the ACME ALPN challenge is not supported,
+ * 1 on failure
+ */
+static int
+daemon_setup (struct MHD_Daemon *d,
+ enum MHD_TlsBackend be,
+ uint16_t *p_port)
+{
+ union MHD_DaemonInfoFixedData info;
+ enum MHD_StatusCode sc;
+
+ if (MHD_SC_OK !=
+ MHD_DAEMON_SET_OPTIONS (
+ d,
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_DEFAULT_TIMEOUT_MILSEC (2000),
+ MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
+ 0),
+ MHD_D_OPTION_TLS (be),
+ /* The certificate parameter is not declared "const" */
+ MHD_D_OPTION_TLS_CERT_KEY ((char *)mhd_DROP_CONST (
+ mhdt_ACME_SRV_CERT_PEM),
+ mhdt_ACME_SRV_KEY_PEM,
+ NULL)))
+ {
+ fprintf (stderr,
+ "Failed to configure the daemon.\n");
+ return 1;
+ }
+
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ mhdt_ACME_CHLNG_A_DOMAIN,
+ mhdt_ACME_CHLNG_A_CERT_PEM,
+ mhdt_ACME_CHLNG_A_KEY_PEM,
+ NULL);
+ if (!check_status (sc,
+ MHD_SC_TOO_EARLY,
+ "adding the certificate before the daemon start"))
+ return 1;
+
+ sc = MHD_daemon_acme_alpn_cert_del (d,
+ mhdt_ACME_CHLNG_A_DOMAIN);
+ if (!check_status (sc,
+ MHD_SC_TOO_EARLY,
+ "removing the certificate before the daemon start"))
+ return 1;
+
+ sc = MHD_daemon_start (d);
+ if (MHD_SC_OK != sc)
+ {
+ fprintf (stderr,
+ "Failed to start the daemon: %u\n",
+ (unsigned int)sc);
+ return 1;
+ }
+
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ mhdt_ACME_CHLNG_A_DOMAIN,
+ mhdt_ACME_CHLNG_A_CERT_PEM,
+ mhdt_ACME_CHLNG_A_KEY_PEM,
+ NULL);
+ if (MHD_SC_TLS_BACKEND_OPERATION_UNSUPPORTED == sc)
+ {
+ fprintf (stderr,
+ "The ACME ALPN challenge is not supported, skipping.\n");
+ return 77;
+ }
+ if (MHD_SC_OK == sc)
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ mhdt_ACME_CHLNG_B_DOMAIN,
+ mhdt_ACME_CHLNG_B_CERT_PEM,
+ mhdt_ACME_CHLNG_B_KEY_PEM,
+ NULL);
+ if (MHD_SC_OK != sc)
+ {
+ fprintf (stderr,
+ "MHD_daemon_acme_alpn_cert_add() failed: %u\n",
+ (unsigned int)sc);
+ return 1;
+ }
+
+ if (MHD_SC_OK !=
+ MHD_daemon_get_info_fixed (d,
+ MHD_DAEMON_INFO_FIXED_BIND_PORT,
+ &info))
+ {
+ fprintf (stderr,
+ "Failed to get the port of the daemon.\n");
+ return 1;
+ }
+ *p_port = info.v_bind_port_uint16;
+ return 0;
+}
+
+
+/**
+ * Create and start the daemon with the ACME challenge certificates.
+ *
+ * @param be the TLS backend to use
+ * @param[out] p_d the started daemon, set on success only
+ * @param[out] p_port the port bound by the daemon, set on success only
+ * @return 0 on success,
+ * 77 if the ACME ALPN challenge is not supported,
+ * 1 on failure
+ */
+static int
+start_daemon (enum MHD_TlsBackend be,
+ struct MHD_Daemon **p_d,
+ uint16_t *p_port)
+{
+ struct MHD_Daemon *d;
+ int res;
+
+ *p_d = NULL;
+ d = MHD_daemon_create (&server_req_cb,
+ NULL);
+ if (NULL == d)
+ {
+ fprintf (stderr,
+ "MHD_daemon_create() failed.\n");
+ return 1;
+ }
+
+ res = daemon_setup (d,
+ be,
+ p_port);
+ if (0 == res)
+ {
+ *p_d = d;
+ return 0; /* Success exit point */
+ }
+
+ /* Below is a clean-up path */
+ MHD_daemon_destroy (d);
+ return res; /* Failure exit point */
+}
+
+
+/**
+ * Check that the certificate management functions reject the wrong use.
+ *
+ * @param d the started daemon
+ * @return 'true' if all the calls are rejected as expected,
+ * 'false' otherwise
+ */
+static bool
+check_cert_api_misuse (struct MHD_Daemon *d)
+{
+ enum MHD_StatusCode sc;
+ bool res;
+
+ res = true;
+
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ "",
+ mhdt_ACME_CHLNG_A_CERT_PEM,
+ mhdt_ACME_CHLNG_A_KEY_PEM,
+ NULL);
+ if (!check_status (sc,
+ MHD_SC_PARAM_EMPTY,
+ "adding the certificate for the empty domain"))
+ res = false;
+
+ sc = MHD_daemon_acme_alpn_cert_del (d,
+ "");
+ if (!check_status (sc,
+ MHD_SC_PARAM_EMPTY,
+ "removing the certificate of the empty domain"))
+ res = false;
+
+ sc = MHD_daemon_acme_alpn_cert_del (d,
+ DOMAIN_NO_ACME);
+ if (!check_status (sc,
+ MHD_SC_ITEM_NOT_FOUND,
+ "removing the certificate of the unknown domain"))
+ res = false;
+
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ DOMAIN_BAD_CERT,
+ "",
+ mhdt_ACME_CHLNG_A_KEY_PEM,
+ NULL);
+ if (!check_status (sc,
+ MHD_SC_TLS_CONF_BAD_CERT,
+ "adding the empty certificate"))
+ res = false;
+
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ DOMAIN_BAD_CERT,
+ BAD_CERT_PEM,
+ mhdt_ACME_CHLNG_A_KEY_PEM,
+ NULL);
+ if (!check_status (sc,
+ MHD_SC_TLS_CONF_BAD_CERT,
+ "adding the malformed certificate"))
+ res = false;
+
+ sc = MHD_daemon_acme_alpn_cert_add (d,
+ DOMAIN_BAD_CERT,
+ mhdt_ACME_CHLNG_A_CERT_PEM,
+ mhdt_ACME_CHLNG_B_KEY_PEM,
+ NULL);
+ if (!check_status (sc,
+ MHD_SC_TLS_CONF_BAD_CERT,
+ "adding the certificate with the key of another one"))
+ res = false;
+
+ return res;
+}
+
+
+/**
+ * Perform a single test case and report the failure
+ *
+ * @param port the port of the daemon
+ * @param tc the test case to perform
+ * @return 'true' if the results are as expected,
+ * 'false' otherwise
+ */
+static bool
+run_case (uint16_t port,
+ const struct TestCase *tc)
+{
+ fprintf (stderr,
+ "Checking %s.\n",
+ tc->label);
+ if (run_test_case (port,
+ tc))
+ return true;
+
+ fprintf (stderr,
+ "FAILED: %s\n",
+ tc->label);
+ return false;
+}
+
+
+/**
+ * Perform all the test cases with the specified TLS backend.
+ *
+ * @param be the TLS backend to use
+ * @param be_name the name of the backend for the log
+ * @return 0 on success,
+ * 77 if the ACME ALPN challenge is not supported,
+ * 1 if any test case failed
+ */
+static int
+run_backend (enum MHD_TlsBackend be,
+ const char *be_name)
+{
+ struct MHD_Daemon *d;
+ uint16_t port;
+ unsigned int i;
+ int res;
+
+ fprintf (stderr,
+ "Testing with the TLS backend \"%s\".\n",
+ be_name);
+ res = start_daemon (be,
+ &d,
+ &port);
+ if (0 != res)
+ return res;
+
+ if (!check_cert_api_misuse (d))
+ res = 1;
+
+ for (i = 0; i < (sizeof(test_cases) / sizeof(test_cases[0])); ++i)
+ {
+ if (!run_case (port,
+ test_cases + i))
+ res = 1;
+ }
+
+ /* The certificate for the first domain is removed, the domain name is
+ spelled in the different letters case */
+ if (MHD_SC_OK !=
+ MHD_daemon_acme_alpn_cert_del (d,
+ DOMAIN_A_MIXED_CASE))
+ {
+ fprintf (stderr,
+ "MHD_daemon_acme_alpn_cert_del() failed.\n");
+ res = 1;
+ }
+ else if (!run_case (port,
+ &removed_case))
+ res = 1;
+
+ /* The same certificate is registered for any domain */
+ if (MHD_SC_OK !=
+ MHD_daemon_acme_alpn_cert_add (d,
+ NULL,
+ mhdt_ACME_CHLNG_A_CERT_PEM,
+ mhdt_ACME_CHLNG_A_KEY_PEM,
+ NULL))
+ {
+ fprintf (stderr,
+ "Failed to add the fallback certificate.\n");
+ res = 1;
+ }
+ else
+ {
+ if (!run_case (port,
+ &fallback_case))
+ res = 1;
+ if (!run_case (port,
+ &fallback_no_sni_case))
+ res = 1;
+ if (!run_case (port,
+ &fallback_specific_case))
+ res = 1;
+ }
+
+ /* The certificate of the first domain is set to another certificate */
+ if (MHD_SC_OK !=
+ MHD_daemon_acme_alpn_cert_add (d,
+ mhdt_ACME_CHLNG_A_DOMAIN,
+ mhdt_ACME_CHLNG_B_CERT_PEM,
+ mhdt_ACME_CHLNG_B_KEY_PEM,
+ NULL))
+ {
+ fprintf (stderr,
+ "Failed to replace the challenge certificate.\n");
+ res = 1;
+ }
+ else if (!run_case (port,
+ &replaced_case))
+ res = 1;
+
+ /* All the challenge certificates are removed by the single call */
+ if (MHD_SC_OK !=
+ MHD_daemon_acme_alpn_cert_del (d,
+ NULL))
+ {
+ fprintf (stderr,
+ "Failed to remove all the challenge certificates.\n");
+ res = 1;
+ }
+ else if (!run_case (port,
+ &cleared_case))
+ res = 1;
+
+ /* Unlike the removal by the domain name, the removal of all the
+ certificates is not an error when nothing is left to remove */
+ if (MHD_SC_OK !=
+ MHD_daemon_acme_alpn_cert_del (d,
+ NULL))
+ {
+ fprintf (stderr,
+ "The repeated removal of all the certificates failed.\n");
+ res = 1;
+ }
+
+ MHD_daemon_destroy (d);
+
+ if (unexpected_request)
+ {
+ fprintf (stderr,
+ "FAILED: the daemon has received a request.\n");
+ unexpected_request = false;
+ res = 1;
+ }
+ return res;
+}
+
+
+int
+main (int argc,
+ char *argv[])
+{
+ bool tested_one_or_more;
+ bool failed;
+ int res;
+
+ (void)argc; /* Unused */
+ (void)argv; /* Unused */
+
+ if (GNUTLS_E_SUCCESS != gnutls_global_init ())
+ {
+ fprintf (stderr,
+ "gnutls_global_init() failed.\n");
+ return 99;
+ }
+ tested_one_or_more = false;
+ failed = false;
+
+ res = run_backend (MHD_TLS_BACKEND_GNUTLS,
+ "GnuTLS");
+ if (77 != res)
+ {
+ tested_one_or_more = true;
+ if (0 != res)
+ failed = true;
+ }
+# ifdef MHD_SUPPORT_OPENSSL
+ res = run_backend (MHD_TLS_BACKEND_OPENSSL,
+ "OpenSSL");
+ if (77 != res)
+ {
+ tested_one_or_more = true;
+ if (0 != res)
+ failed = true;
+ }
+# endif /* MHD_SUPPORT_OPENSSL */
+
+ gnutls_global_deinit ();
+
+ if (!tested_one_or_more)
+ return 77;
+
+ return failed ? 1 : 0;
+}
+
+
+#else /* GnuTLS before 3.2.0 */
+
+int
+main (void)
+{
+ fprintf (stderr,
+ "GnuTLS has no ALPN support, skipping.\n");
+ return 77;
+}
+
+
+#endif /* GnuTLS before 3.2.0 */