libmicrohttpd2

HTTP server C library (MHD 2.x, alpha)
Log | Files | Refs | README | LICENSE

test_raw.c (32447B)


      1 /* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
      2 /*
      3   This file is part of GNU libmicrohttpd.
      4   Copyright (C) 2025 Christian Grothoff
      5 
      6   GNU libmicrohttpd is free software; you can redistribute it and/or
      7   modify it under the terms of the GNU Lesser General Public
      8   License as published by the Free Software Foundation; either
      9   version 2.1 of the License, or (at your option) any later version.
     10 
     11   GNU libmicrohttpd is distributed in the hope that it will be useful,
     12   but WITHOUT ANY WARRANTY; without even the implied warranty of
     13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     14   Lesser General Public License for more details.
     15 
     16   Alternatively, you can redistribute GNU libmicrohttpd and/or
     17   modify it under the terms of the GNU General Public License as
     18   published by the Free Software Foundation; either version 2 of
     19   the License, or (at your option) any later version, together
     20   with the eCos exception, as follows:
     21 
     22     As a special exception, if other files instantiate templates or
     23     use macros or inline functions from this file, or you compile this
     24     file and link it with other works to produce a work based on this
     25     file, this file does not by itself cause the resulting work to be
     26     covered by the GNU General Public License. However the source code
     27     for this file must still be made available in accordance with
     28     section (3) of the GNU General Public License v2.
     29 
     30     This exception does not invalidate any other reasons why a work
     31     based on this file might be covered by the GNU General Public
     32     License.
     33 
     34   You should have received copies of the GNU Lesser General Public
     35   License and the GNU General Public License along with this library;
     36   if not, see <https://www.gnu.org/licenses/>.
     37 */
     38 
     39 /**
     40  * @file test_raw.c
     41  * @brief tests streams against server
     42  * @author Christian Grothoff
     43  */
     44 #include <stdio.h>
     45 #include <stdbool.h>
     46 #include <errno.h>
     47 #include <string.h>
     48 #include <stdlib.h>
     49 #include <unistd.h>
     50 #include <arpa/inet.h>
     51 #include <netinet/ip.h>
     52 #include "microhttpd2.h"
     53 
     54 
     55 /**
     56  * Defines a test.
     57  */
     58 struct Test
     59 {
     60   /**
     61    * Human-readable name of the test. NULL to end test array.
     62    */
     63   const char *name;
     64 
     65   /**
     66    * Request to send to the server.
     67    */
     68   const char *upload;
     69 
     70   /**
     71    * Expected HTTP method.
     72    */
     73   enum MHD_HTTP_Method expect_method;
     74 
     75   /**
     76    * Expected path.
     77    */
     78   const char *expect_path;
     79 
     80   /**
     81    * Expected upload size.
     82    */
     83   uint_fast64_t expect_upload_size;
     84 
     85   /**
     86    * Special string indicating what the MHD parser should give us.
     87    * Can be used to encode expected HTTP headers using
     88    * "H-$KEY:$VALUE" or HTTP cookies using "C-$KEY:$VALUE".
     89    * Multiple entries are separated via "\n".
     90    */
     91   const char *expect_parser;
     92 };
     93 
     94 
     95 static struct Test tests[] = {
     96   {
     97     .name = "Basic GET",
     98     .upload = "GET / HTTP/1.0\r\n\r\n",
     99     .expect_method = MHD_HTTP_METHOD_GET,
    100     .expect_path = "/",
    101   },
    102   {
    103     .name = "Basic HTTP/1.1 GET",
    104     .upload = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
    105     .expect_method = MHD_HTTP_METHOD_GET,
    106     .expect_path = "/",
    107     .expect_parser = "H-Host:example.com",
    108   },
    109   {
    110     /* HTTP/1.2 SHOULD be processed as 1.1 if server supports 1.1.
    111        RFC 9110, Section 6.2
    112        See https://www.rfc-editor.org/rfc/rfc9110.html#name-control-data */
    113     .name = "Basic HTTP/1.2 GET",
    114     .upload = "GET / HTTP/1.2\r\nHost: example.com\r\n\r\n",
    115     .expect_method = MHD_HTTP_METHOD_GET,
    116     .expect_path = "/",
    117     .expect_parser = "H-Host:example.com",
    118   },
    119   {
    120     /* HTTP/1.9 SHOULD be processed as 1.1 if server supports 1.1.
    121        RFC 9110, Section 6.2
    122        See https://www.rfc-editor.org/rfc/rfc9110.html#name-control-data */
    123     .name = "Basic HTTP/1.9 GET",
    124     .upload = "GET / HTTP/1.9\r\nHost: example.com\r\n\r\n",
    125     .expect_method = MHD_HTTP_METHOD_GET,
    126     .expect_path = "/",
    127     .expect_parser = "H-Host:example.com",
    128   },
    129   {
    130     .name = "Multi-header HTTP/1.1 GET with query parameters",
    131     .upload = "GET /?k=v&a HTTP/1.1\r\nHost: example.com\r\nKey: value\r\n\r\n",
    132     .expect_method = MHD_HTTP_METHOD_GET,
    133     .expect_path = "/",
    134     .expect_parser = "H-Host:example.com\nH-Key:value\nQ-k:v\nQ-a",
    135   },
    136   {
    137     .name = "Empty header value",
    138     .upload = "GET / HTTP/1.1\r\nHost: example.com\r\nX-Empty:\r\n\r\n",
    139     .expect_method = MHD_HTTP_METHOD_GET,
    140     .expect_path = "/",
    141     .expect_parser = "H-Host:example.com\nH-X-Empty:",
    142   },
    143   {
    144     .name = "Header with leading/trailing whitespace",
    145     .upload =
    146       "GET / HTTP/1.1\r\nHost: example.com\r\nX-Space:  value  \r\n\r\n",
    147     .expect_method = MHD_HTTP_METHOD_GET,
    148     .expect_path = "/",
    149     .expect_parser = "H-Host:example.com\nH-X-Space:value",
    150   },
    151   {
    152     .name = "Multiple headers with same name",
    153     .upload =
    154       "GET / HTTP/1.1\r\nHost: example.com\r\nX-Dup: first\r\nX-Dup: second\r\n\r\n",
    155     .expect_method = MHD_HTTP_METHOD_GET,
    156     .expect_path = "/",
    157     .expect_parser = "H-Host:example.com\nH-X-Dup:first",
    158   },
    159   {
    160     .name = "Case insensitive header names",
    161     .upload =
    162       "GET / HTTP/1.1\r\nhost: example.com\r\nContent-TYPE: text/plain\r\n\r\n",
    163     .expect_method = MHD_HTTP_METHOD_GET,
    164     .expect_path = "/",
    165     .expect_parser = "H-Host:example.com\nH-Content-Type:text/plain",
    166   },
    167   {
    168     .name = "Query parameter with empty value",
    169     .upload = "GET /?key= HTTP/1.1\r\nHost: example.com\r\n\r\n",
    170     .expect_method = MHD_HTTP_METHOD_GET,
    171     .expect_path = "/",
    172     .expect_parser = "H-Host:example.com\nQ-key:",
    173   },
    174   {
    175     .name = "Query parameter without value",
    176     .upload = "GET /?flag HTTP/1.1\r\nHost: example.com\r\n\r\n",
    177     .expect_method = MHD_HTTP_METHOD_GET,
    178     .expect_path = "/",
    179     .expect_parser = "H-Host:example.com\nQ-flag",
    180   },
    181   {
    182     .name = "Multiple query parameters",
    183     .upload = "GET /?a=1&b=2&c=3 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    184     .expect_method = MHD_HTTP_METHOD_GET,
    185     .expect_path = "/",
    186     .expect_parser = "H-Host:example.com\nQ-a:1\nQ-b:2\nQ-c:3",
    187   },
    188   {
    189     .name = "URL encoded query parameter",
    190     .upload =
    191       "GET /?name=hello%20world&special=%21%40%23 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    192     .expect_method = MHD_HTTP_METHOD_GET,
    193     .expect_path = "/",
    194     .expect_parser = "H-Host:example.com\nQ-name:hello world\nQ-special:!@#",
    195   },
    196   {
    197     .name = "Simple cookie",
    198     .upload =
    199       "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: session=abc123\r\n\r\n",
    200     .expect_method = MHD_HTTP_METHOD_GET,
    201     .expect_path = "/",
    202     .expect_parser = "H-Host:example.com\nC-session:abc123",
    203   },
    204   {
    205     .name = "Multiple cookies in one header",
    206     .upload =
    207       "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: a=1; b=2; c=3\r\n\r\n",
    208     .expect_method = MHD_HTTP_METHOD_GET,
    209     .expect_path = "/",
    210     .expect_parser = "H-Host:example.com\nC-a:1\nC-b:2\nC-c:3",
    211   },
    212 #if COMPATIBILITY_QUESTION
    213   {
    214     .name = "Cookie with spaces",
    215     .upload =
    216       "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: key = value\r\n\r\n",
    217     .expect_method = MHD_HTTP_METHOD_GET,
    218     .expect_path = "/",
    219     .expect_parser = "H-Host:example.com\nC-key:value",
    220   },
    221 #endif
    222   {
    223     .name = "POST with Content-Length",
    224     .upload =
    225       "POST /submit HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello",
    226     .expect_method = MHD_HTTP_METHOD_POST,
    227     .expect_path = "/submit",
    228     .expect_upload_size = 5,
    229     .expect_parser = "H-Host:example.com\nH-Content-Length:5",
    230   },
    231 #if VERY_BAD_BUG
    232   {
    233     .name = "Path with special characters",
    234     .upload =
    235       "GET /path/to/resource%20file.html HTTP/1.1\r\nHost: example.com\r\n\r\n",
    236     .expect_method = MHD_HTTP_METHOD_GET,
    237     .expect_path = "/path/to/resource file.html",
    238     .expect_parser = "H-Host:example.com",
    239   },
    240 #endif
    241   {
    242     .name = "Long header value",
    243     .upload =
    244       "GET / HTTP/1.1\r\nHost: example.com\r\nX-Long: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n",
    245     .expect_method = MHD_HTTP_METHOD_GET,
    246     .expect_path = "/",
    247     .expect_parser =
    248       "H-Host:example.com\nH-X-Long:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    249   },
    250   {
    251     .name = "Query string with equals sign in value",
    252     .upload = "GET /?expr=a=b HTTP/1.1\r\nHost: example.com\r\n\r\n",
    253     .expect_method = MHD_HTTP_METHOD_GET,
    254     .expect_path = "/",
    255     .expect_parser = "H-Host:example.com\nQ-expr:a=b",
    256   },
    257   {
    258     .name = "PATCH request",
    259     .upload =
    260       "PATCH /resource HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n",
    261     .expect_method = MHD_HTTP_METHOD_OTHER,
    262     .expect_path = "/resource",
    263     .expect_upload_size = 0,
    264     .expect_parser = "H-Host:example.com\nH-Content-Length:0",
    265   },
    266   {
    267     .name = "Mixed query parameters with and without values",
    268     .upload =
    269       "GET /?debug&level=5&verbose HTTP/1.1\r\nHost: example.com\r\n\r\n",
    270     .expect_method = MHD_HTTP_METHOD_GET,
    271     .expect_path = "/",
    272     .expect_parser = "H-Host:example.com\nQ-debug\nQ-level:5\nQ-verbose",
    273   },
    274 #if PARSER_BUG
    275   {
    276     .name = "Continuation header (folded)",
    277     .upload =
    278       "GET / HTTP/1.1\r\nHost: example.com\r\nX-Multi: line one\r\n line two\r\n\r\n",
    279     .expect_method = MHD_HTTP_METHOD_GET,
    280     .expect_path = "/",
    281     .expect_parser = "H-Host:example.com\nH-X-Multi:line one line two",
    282   },
    283 #endif
    284   {
    285     .name = "Empty query string",
    286     .upload = "GET /? HTTP/1.1\r\nHost: example.com\r\n\r\n",
    287     .expect_method = MHD_HTTP_METHOD_GET,
    288     .expect_path = "/",
    289     .expect_parser = "H-Host:example.com",
    290   },
    291 #if UNDERSPECIFIED_LIKELY_BAD
    292   {
    293     .name = "Fragment in URI (should probably be stripped!)",
    294     .upload = "GET /page#section HTTP/1.1\r\nHost: example.com\r\n\r\n",
    295     .expect_method = MHD_HTTP_METHOD_GET,
    296     .expect_path = "/page",
    297     .expect_parser = "H-Host:example.com",
    298   },
    299 #endif
    300   {
    301     .name = "PUT request with headers",
    302     .upload =
    303       "PUT /resource HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nContent-Length: 0\r\n\r\n",
    304     .expect_method = MHD_HTTP_METHOD_PUT,
    305     .expect_path = "/resource",
    306     .expect_upload_size = 0,
    307     .expect_parser =
    308       "H-Host:example.com\nH-Content-Type:application/json\nH-Content-Length:0",
    309   },
    310   {
    311     .name = "DELETE request",
    312     .upload = "DELETE /resource/123 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    313     .expect_method = MHD_HTTP_METHOD_DELETE,
    314     .expect_path = "/resource/123",
    315     .expect_parser = "H-Host:example.com",
    316   },
    317 
    318   {
    319     .name = "Header with colon in value",
    320     .upload = "GET / HTTP/1.1\r\nHost: example.com\r\nX-Time: 12:34:56\r\n\r\n",
    321     .expect_method = MHD_HTTP_METHOD_GET,
    322     .expect_path = "/",
    323     .expect_parser = "H-Host:example.com\nH-X-Time:12:34:56",
    324   },
    325   {
    326     .name = "Cookie with empty value",
    327     .upload = "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: empty=\r\n\r\n",
    328     .expect_method = MHD_HTTP_METHOD_GET,
    329     .expect_path = "/",
    330     .expect_parser = "H-Host:example.com\nC-empty:",
    331   },
    332   {
    333     .name = "Query parameter with plus sign encoding",
    334     .upload = "GET /?text=hello+world HTTP/1.1\r\nHost: example.com\r\n\r\n",
    335     .expect_method = MHD_HTTP_METHOD_GET,
    336     .expect_path = "/",
    337     .expect_parser = "H-Host:example.com\nQ-text:hello world",
    338   },
    339   {
    340     .name = "Multiple Cookie headers",
    341     .upload =
    342       "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: a=1\r\nCookie: b=2\r\n\r\n",
    343     .expect_method = MHD_HTTP_METHOD_GET,
    344     .expect_path = "/",
    345     .expect_parser = "H-Host:example.com\nC-a:1\nC-b:2",
    346   },
    347   {
    348     .name = "HEAD request",
    349     .upload = "HEAD /resource HTTP/1.1\r\nHost: example.com\r\n\r\n",
    350     .expect_method = MHD_HTTP_METHOD_HEAD,
    351     .expect_path = "/resource",
    352     .expect_parser = "H-Host:example.com",
    353   },
    354   {
    355     .name = "OPTIONS request",
    356     .upload = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n",
    357     .expect_method = MHD_HTTP_METHOD_OPTIONS,
    358     .expect_path = "*",
    359     .expect_parser = "H-Host:example.com",
    360   },
    361   {
    362     .name = "Query with ampersand in value",
    363     .upload = "GET /?code=a%26b HTTP/1.1\r\nHost: example.com\r\n\r\n",
    364     .expect_method = MHD_HTTP_METHOD_GET,
    365     .expect_path = "/",
    366     .expect_parser = "H-Host:example.com\nQ-code:a&b",
    367   },
    368   {
    369     .name = "Query with percent sign in value",
    370     .upload = "GET /?percent=100%25 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    371     .expect_method = MHD_HTTP_METHOD_GET,
    372     .expect_path = "/",
    373     .expect_parser = "H-Host:example.com\nQ-percent:100%",
    374   },
    375   {
    376     .name = "Root path with trailing slash",
    377     .upload = "GET // HTTP/1.1\r\nHost: example.com\r\n\r\n",
    378     .expect_method = MHD_HTTP_METHOD_GET,
    379     .expect_path = "//",
    380     .expect_parser = "H-Host:example.com",
    381   },
    382   {
    383     .name = "Deep path nesting",
    384     .upload = "GET /a/b/c/d/e/f/g HTTP/1.1\r\nHost: example.com\r\n\r\n",
    385     .expect_method = MHD_HTTP_METHOD_GET,
    386     .expect_path = "/a/b/c/d/e/f/g",
    387     .expect_parser = "H-Host:example.com",
    388   },
    389   {
    390     .name = "Authorization header",
    391     .upload =
    392       "GET / HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer token123\r\n\r\n",
    393     .expect_method = MHD_HTTP_METHOD_GET,
    394     .expect_path = "/",
    395     .expect_parser = "H-Host:example.com\nH-Authorization:Bearer token123",
    396   },
    397   {
    398     .name = "User-Agent header",
    399     .upload =
    400       "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\n\r\n",
    401     .expect_method = MHD_HTTP_METHOD_GET,
    402     .expect_path = "/",
    403     .expect_parser = "H-Host:example.com\nH-User-Agent:Mozilla/5.0",
    404   },
    405   {
    406     .name = "Accept header with multiple values",
    407     .upload =
    408       "GET / HTTP/1.1\r\nHost: example.com\r\nAccept: text/html, application/json\r\n\r\n",
    409     .expect_method = MHD_HTTP_METHOD_GET,
    410     .expect_path = "/",
    411     .expect_parser = "H-Host:example.com\nH-Accept:text/html, application/json",
    412   },
    413   {
    414     .name = "Connection keep-alive",
    415     .upload =
    416       "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n",
    417     .expect_method = MHD_HTTP_METHOD_GET,
    418     .expect_path = "/",
    419     .expect_parser = "H-Host:example.com\nH-Connection:keep-alive",
    420   },
    421   {
    422     .name = "Query with duplicate parameters",
    423     .upload = "GET /?id=1&id=2&id=3 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    424     .expect_method = MHD_HTTP_METHOD_GET,
    425     .expect_path = "/",
    426     .expect_parser = "H-Host:example.com\nQ-id:1",
    427   },
    428   {
    429     .name = "Cookie with quoted value",
    430     .upload =
    431       "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: session=\"abc123\"\r\n\r\n",
    432     .expect_method = MHD_HTTP_METHOD_GET,
    433     .expect_path = "/",
    434     .expect_parser = "H-Host:example.com\nC-session:abc123",
    435   },
    436   {
    437     .name = "Query with UTF-8 encoding",
    438     .upload = "GET /?name=%E2%9C%93 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    439     .expect_method = MHD_HTTP_METHOD_GET,
    440     .expect_path = "/",
    441     .expect_parser = "H-Host:example.com\nQ-name:✓",
    442   },
    443   {
    444     .name = "Referer header",
    445     .upload =
    446       "GET /page HTTP/1.1\r\nHost: example.com\r\nReferer: https://google.com\r\n\r\n",
    447     .expect_method = MHD_HTTP_METHOD_GET,
    448     .expect_path = "/page",
    449     .expect_parser = "H-Host:example.com\nH-Referer:https://google.com",
    450   },
    451   {
    452     .name = "Content-Type with charset",
    453     .upload =
    454       "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 0\r\n\r\n",
    455     .expect_method = MHD_HTTP_METHOD_POST,
    456     .expect_path = "/",
    457     .expect_upload_size = 0,
    458     .expect_parser =
    459       "H-Host:example.com\nH-Content-Type:text/html; charset=utf-8\nH-Content-Length:0",
    460   },
    461   {
    462     .name = "Cache-Control header",
    463     .upload =
    464       "GET / HTTP/1.1\r\nHost: example.com\r\nCache-Control: no-cache\r\n\r\n",
    465     .expect_method = MHD_HTTP_METHOD_GET,
    466     .expect_path = "/",
    467     .expect_parser = "H-Host:example.com\nH-Cache-Control:no-cache",
    468   },
    469   {
    470     .name = "If-None-Match header",
    471     .upload =
    472       "GET / HTTP/1.1\r\nHost: example.com\r\nIf-None-Match: \"etag123\"\r\n\r\n",
    473     .expect_method = MHD_HTTP_METHOD_GET,
    474     .expect_path = "/",
    475     .expect_parser = "H-Host:example.com\nH-If-None-Match:\"etag123\"",
    476   },
    477   {
    478     .name = "Range header",
    479     .upload =
    480       "GET /file HTTP/1.1\r\nHost: example.com\r\nRange: bytes=0-1023\r\n\r\n",
    481     .expect_method = MHD_HTTP_METHOD_GET,
    482     .expect_path = "/file",
    483     .expect_parser = "H-Host:example.com\nH-Range:bytes=0-1023",
    484   },
    485   {
    486     .name = "X-Forwarded-For header",
    487     .upload =
    488       "GET / HTTP/1.1\r\nHost: example.com\r\nX-Forwarded-For: 192.168.1.1\r\n\r\n",
    489     .expect_method = MHD_HTTP_METHOD_GET,
    490     .expect_path = "/",
    491     .expect_parser = "H-Host:example.com\nH-X-Forwarded-For:192.168.1.1",
    492   },
    493   {
    494     .name = "Query with empty parameter name",
    495     .upload = "GET /?=value HTTP/1.1\r\nHost: example.com\r\n\r\n",
    496     .expect_method = MHD_HTTP_METHOD_GET,
    497     .expect_path = "/",
    498     .expect_parser = "H-Host:example.com\nQ-:value",
    499   },
    500 #if QUESTIONABLE_TEST
    501   {
    502     .name = "Absolute URI in request line",
    503     .upload = "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
    504     .expect_method = MHD_HTTP_METHOD_GET,
    505     .expect_path = "/",
    506     .expect_parser = "H-Host:example.com",
    507   },
    508 #endif
    509   {
    510     .name = "Query with semicolon separator",
    511     .upload = "GET /?a=1;b=2 HTTP/1.1\r\nHost: example.com\r\n\r\n",
    512     .expect_method = MHD_HTTP_METHOD_GET,
    513     .expect_path = "/",
    514     .expect_parser = "H-Host:example.com\nQ-a:1;b=2",
    515   },
    516   {
    517     .name = "Cookie with special characters",
    518     .upload =
    519       "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: data=value_with-special.chars\r\n\r\n",
    520     .expect_method = MHD_HTTP_METHOD_GET,
    521     .expect_path = "/",
    522     .expect_parser = "H-Host:example.com\nC-data:value_with-special.chars",
    523   },
    524   {
    525     .name = "Minimal HTTP/1.0 request with path only",
    526     .upload = "GET /path HTTP/1.0\r\n\r\n",
    527     .expect_method = MHD_HTTP_METHOD_GET,
    528     .expect_path = "/path",
    529   },
    530   {
    531     .name = "POST with form data Content-Type",
    532     .upload =
    533       "POST /form HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 0\r\n\r\n",
    534     .expect_method = MHD_HTTP_METHOD_POST,
    535     .expect_path = "/form",
    536     .expect_upload_size = 0,
    537     .expect_parser =
    538       "H-Host:example.com\nH-Content-Type:application/x-www-form-urlencoded\nH-Content-Length:0",
    539   },
    540   {
    541     .name = "Query with trailing ampersand",
    542     .upload = "GET /?a=1& HTTP/1.1\r\nHost: example.com\r\n\r\n",
    543     .expect_method = MHD_HTTP_METHOD_GET,
    544     .expect_path = "/",
    545     .expect_parser = "H-Host:example.com\nQ-a:1",
    546   },
    547 #if BUG
    548   {
    549     .name = "Absolute form",
    550     // RFC 9112 Section 3.2.2
    551     .upload = "GET http://example.com/ HTTP/1.1\r\nHost: example.bad\r\n\r\n",
    552     .expect_method = MHD_HTTP_METHOD_GET,
    553     .expect_path = "/",
    554     .expect_parser = "H-Host:example.com",
    555   },
    556 #endif
    557   {
    558     .name = NULL,
    559   }
    560 };
    561 
    562 
    563 static struct Test *current;
    564 
    565 
    566 /**
    567  * Our port.
    568  */
    569 static uint16_t port;
    570 
    571 
    572 static bool
    573 check_headers (struct MHD_Request *MHD_RESTRICT request,
    574                const char *spec)
    575 {
    576   bool ok = true;
    577   char *hdr = strdup (spec);
    578   char *tok;
    579   char key[strlen (spec)];
    580   char value[strlen (spec)];
    581 
    582   for (tok = strtok (hdr,
    583                      "\n");
    584        NULL != tok;
    585        tok = strtok (NULL,
    586                      "\n"))
    587   {
    588     char dummy;
    589 
    590     if (2 ==
    591         sscanf (tok,
    592                 "H-%[^:]:%[^\n]s",
    593                 key,
    594                 value))
    595     {
    596       struct MHD_StringNullable have;
    597 
    598       if (! MHD_request_get_value (request,
    599                                    MHD_VK_HEADER,
    600                                    key,
    601                                    &have))
    602       {
    603         fprintf (stderr,
    604                  "Expected header `%s' missing\n",
    605                  key);
    606         ok = false;
    607       }
    608       else if ( (NULL == have.cstr) ||
    609                 (0 != strcmp (have.cstr,
    610                               value)) )
    611       {
    612         fprintf (stderr,
    613                  "Wrong header value `%s' under key `%s', expected `%s'\n",
    614                  have.cstr,
    615                  key,
    616                  value);
    617         ok = false;
    618       }
    619     }
    620     else if (2 ==
    621              sscanf (tok,
    622                      "C-%[^:]:%[^\n]",
    623                      key,
    624                      value))
    625     {
    626       struct MHD_StringNullable have;
    627 
    628       if (! MHD_request_get_value (request,
    629                                    MHD_VK_COOKIE,
    630                                    key,
    631                                    &have))
    632       {
    633         fprintf (stderr,
    634                  "Expected cookie `%s' missing\n",
    635                  key);
    636         ok = false;
    637       }
    638       else if ( (NULL == have.cstr) ||
    639                 (0 != strcmp (have.cstr,
    640                               value)) )
    641       {
    642         fprintf (stderr,
    643                  "Wrong cookie value `%s' under key `%s', expected `%s'\n",
    644                  have.cstr,
    645                  key,
    646                  value);
    647         ok = false;
    648       }
    649     }
    650     else if (2 ==
    651              sscanf (tok,
    652                      "Q-%[^:]:%[^\n]",
    653                      key,
    654                      value))
    655     {
    656       struct MHD_StringNullable have;
    657 
    658       if (! MHD_request_get_value (request,
    659                                    MHD_VK_URI_QUERY_PARAM,
    660                                    key,
    661                                    &have))
    662       {
    663         fprintf (stderr,
    664                  "Expected URI query parameter `%s' missing\n",
    665                  key);
    666         ok = false;
    667       }
    668       else if ( (NULL == have.cstr) ||
    669                 (0 != strcmp (have.cstr,
    670                               value)) )
    671       {
    672         fprintf (stderr,
    673                  "Wrong URI query parameter value `%s' under key `%s', expected `%s'\n",
    674                  have.cstr,
    675                  key,
    676                  value);
    677         ok = false;
    678       }
    679     }
    680     else if (1 ==
    681              sscanf (tok,
    682                      "H-%[^:]:%[^\n]",
    683                      key,
    684                      value))
    685     {
    686       struct MHD_StringNullable have;
    687 
    688       if (! MHD_request_get_value (request,
    689                                    MHD_VK_HEADER,
    690                                    key,
    691                                    &have))
    692       {
    693         fprintf (stderr,
    694                  "Expected header `%s' missing\n",
    695                  key);
    696         ok = false;
    697       } /* HTTP header can never be "NULL", only empty string */
    698       else if ( (NULL == have.cstr) ||
    699                 (0 != strcmp (have.cstr,
    700                               "")) )
    701       {
    702         fprintf (stderr,
    703                  "Unexpected non-empty header value `%s' under key `%s'\n",
    704                  have.cstr,
    705                  key);
    706         ok = false;
    707       }
    708     }
    709     else if (2 ==
    710              sscanf (tok,
    711                      "C-%[^:]%c%[^\n]",
    712                      key,
    713                      &dummy,
    714                      value))
    715     {
    716       struct MHD_StringNullable have;
    717 
    718       if (! MHD_request_get_value (request,
    719                                    MHD_VK_COOKIE,
    720                                    key,
    721                                    &have))
    722       {
    723         fprintf (stderr,
    724                  "Expected cookie `%s' missing\n",
    725                  key);
    726         ok = false;
    727       }
    728       else if ( (NULL == have.cstr) ||
    729                 (0 != strcmp (have.cstr,
    730                               "")) )
    731       {
    732         fprintf (stderr,
    733                  "Unexpected non-empty cookie value `%s' under key `%s'\n",
    734                  have.cstr,
    735                  key);
    736         ok = false;
    737       }
    738     }
    739     else if (1 ==
    740              sscanf (tok,
    741                      "C-%[^:]:%[^\n]",
    742                      key,
    743                      value))
    744     {
    745       struct MHD_StringNullable have;
    746 
    747       if (! MHD_request_get_value (request,
    748                                    MHD_VK_COOKIE,
    749                                    key,
    750                                    &have))
    751       {
    752         fprintf (stderr,
    753                  "Expected cookie `%s' missing\n",
    754                  key);
    755         ok = false;
    756       }
    757       else if (NULL != have.cstr)
    758       {
    759         fprintf (stderr,
    760                  "Unexpected non-NULL cookie value `%s' under key `%s'\n",
    761                  have.cstr,
    762                  key);
    763         ok = false;
    764       }
    765     }
    766     else if (2 ==
    767              sscanf (tok,
    768                      "Q-%[^:]%c%[^\n]",
    769                      key,
    770                      &dummy,
    771                      value))
    772     {
    773       struct MHD_StringNullable have;
    774 
    775       if (! MHD_request_get_value (request,
    776                                    MHD_VK_URI_QUERY_PARAM,
    777                                    key,
    778                                    &have))
    779       {
    780         fprintf (stderr,
    781                  "Expected URI query parameter `%s' missing\n",
    782                  key);
    783         ok = false;
    784       }
    785       else if ( (NULL == have.cstr) ||
    786                 (0 != strcmp (have.cstr,
    787                               "")) )
    788       {
    789         fprintf (stderr,
    790                  "Unexpected non-empty URI query parameter value `%s' under key `%s'\n",
    791                  have.cstr,
    792                  key);
    793         ok = false;
    794       }
    795     }
    796     else if (1 ==
    797              sscanf (tok,
    798                      "Q-%[^:]:%[^\n]",
    799                      key,
    800                      value))
    801     {
    802       struct MHD_StringNullable have;
    803 
    804       if (! MHD_request_get_value (request,
    805                                    MHD_VK_URI_QUERY_PARAM,
    806                                    key,
    807                                    &have))
    808       {
    809         fprintf (stderr,
    810                  "Expected URI query parameter `%s' missing\n",
    811                  key);
    812         ok = false;
    813       }
    814       else if (NULL != have.cstr)
    815       {
    816         fprintf (stderr,
    817                  "Unexpected non-NULL URI query parameter value `%s' under key `%s'\n",
    818                  have.cstr,
    819                  key);
    820         ok = false;
    821       }
    822     }
    823     else if (1 ==
    824              sscanf (tok,
    825                      "Q-:%[^\n]",
    826                      value))
    827     {
    828       struct MHD_StringNullable have;
    829 
    830       if (! MHD_request_get_value (request,
    831                                    MHD_VK_URI_QUERY_PARAM,
    832                                    "",
    833                                    &have))
    834       {
    835         fprintf (stderr,
    836                  "Expected URI query parameter without key missing\n");
    837         ok = false;
    838       }
    839       else if ( (NULL == have.cstr) ||
    840                 (0 != strcmp (have.cstr,
    841                               value)) )
    842       {
    843         fprintf (stderr,
    844                  "Wrong URI query parameter value `%s' under missing key, expected `%s'\n",
    845                  have.cstr,
    846                  value);
    847         ok = false;
    848       }
    849     }
    850     else
    851     {
    852       fprintf (stderr,
    853                "Invalid token `%s' in test specification\n",
    854                tok);
    855       ok = false;
    856     }
    857   }
    858   free (hdr);
    859   return ok;
    860 }
    861 
    862 
    863 /**
    864  * A client has requested the given url using the given method
    865  * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
    866  * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc).
    867  * If @a upload_size is not zero and response action is provided by this
    868  * callback, then upload will be discarded and the stream (the connection for
    869  * HTTP/1.1) will be closed after sending the response.
    870  *
    871  * @param cls argument given together with the function
    872  *        pointer when the handler was registered with MHD
    873  * @param request the request object
    874  * @param path the requested uri (without arguments after "?")
    875  * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
    876  *        #MHD_HTTP_METHOD_PUT, etc.)
    877  * @param upload_size the size of the message upload content payload,
    878  *                    #MHD_SIZE_UNKNOWN for chunked uploads (if the
    879  *                    final chunk has not been processed yet)
    880  * @return action how to proceed, NULL
    881  *         if the request must be aborted due to a serious
    882  *         error while handling the request (implies closure
    883  *         of underling data stream, for HTTP/1.1 it means
    884  *         socket closure).
    885  */
    886 static const struct MHD_Action *
    887 server_req_cb (void *cls,
    888                struct MHD_Request *MHD_RESTRICT request,
    889                const struct MHD_String *MHD_RESTRICT path,
    890                enum MHD_HTTP_Method method,
    891                uint_fast64_t upload_size)
    892 {
    893   bool failed = false;
    894 
    895   if (current->expect_method != method)
    896   {
    897     fprintf (stderr,
    898              "Wrong HTTP method\n");
    899     failed = true;
    900   }
    901   if (0 != strcmp (current->expect_path,
    902                    path->cstr))
    903   {
    904     fprintf (stderr,
    905              "Wrong HTTP path %s\n",
    906              path->cstr);
    907     failed = true;
    908   }
    909   if (current->expect_upload_size !=
    910       upload_size)
    911   {
    912     fprintf (stderr,
    913              "Wrong HTTP path %s\n",
    914              path->cstr);
    915     failed = true;
    916   }
    917   if ( (NULL != current->expect_parser) &&
    918        (! check_headers (request,
    919                          current->expect_parser)) )
    920     failed = true;
    921   if (failed)
    922     return NULL;
    923   return MHD_action_from_response (
    924     request,
    925     MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT));
    926 }
    927 
    928 
    929 /**
    930  * Helper function to deal with partial writes.
    931  * Fails hard (calls exit() on failures)!
    932  *
    933  * @param fd where to write to
    934  * @param buf what to write
    935  * @param buf_size number of bytes in @a buf
    936  */
    937 static void
    938 write_all (int fd,
    939            const void *buf,
    940            size_t buf_size)
    941 {
    942   const char *cbuf = buf;
    943   size_t off;
    944 
    945   off = 0;
    946   while (off < buf_size)
    947   {
    948     ssize_t ret;
    949 
    950     ret = write (fd,
    951                  &cbuf[off],
    952                  buf_size - off);
    953     if (ret <= 0)
    954     {
    955       fprintf (stderr,
    956                "Writing %u bytes to %d failed: %s\n",
    957                (unsigned int) (buf_size - off),
    958                fd,
    959                strerror (errno));
    960       exit (1);
    961     }
    962     off += ret;
    963   }
    964 }
    965 
    966 
    967 static int
    968 run_test ()
    969 {
    970   int s;
    971   struct sockaddr_in sa = {
    972     .sin_family = AF_INET,
    973     .sin_port = htons (port),
    974   };
    975   char dummy;
    976 
    977   s = socket (AF_INET, SOCK_STREAM, 0);
    978   if (-1 == s)
    979   {
    980     fprintf (stderr,
    981              "socket() failed: %s\n",
    982              strerror (errno));
    983     return 1;
    984   }
    985   inet_pton (AF_INET,
    986              "127.0.0.1",
    987              &sa.sin_addr);
    988   if (0 != connect (s,
    989                     (struct sockaddr *) &sa,
    990                     sizeof (sa)))
    991   {
    992     fprintf (stderr,
    993              "bind() failed: %s\n",
    994              strerror (errno));
    995     close (s);
    996     return 1;
    997   }
    998   write_all (s,
    999              current->upload,
   1000              strlen (current->upload));
   1001   shutdown (s,
   1002             SHUT_WR);
   1003   if (sizeof (dummy) !=
   1004       read (s,
   1005             &dummy,
   1006             sizeof (dummy)))
   1007   {
   1008     fprintf (stderr,
   1009              "Server closed connection due to error!\n");
   1010     close (s);
   1011     return 1;
   1012   }
   1013   close (s);
   1014   return 0;
   1015 }
   1016 
   1017 
   1018 static int
   1019 run_tests (void)
   1020 {
   1021   for (unsigned int i = 0;
   1022        NULL != tests[i].name;
   1023        i++)
   1024   {
   1025     current = &tests[i];
   1026     if (0 != run_test ())
   1027     {
   1028       fprintf (stderr,
   1029                "Test `%s' failed\n",
   1030                current->name);
   1031       return 1;
   1032     }
   1033   }
   1034   return 0;
   1035 }
   1036 
   1037 
   1038 int
   1039 main (void)
   1040 {
   1041   struct MHD_Daemon *d;
   1042 
   1043   d = MHD_daemon_create (&server_req_cb,
   1044                          NULL);
   1045   if (MHD_SC_OK !=
   1046       MHD_DAEMON_SET_OPTIONS (
   1047         d,
   1048         MHD_D_OPTION_WM_WORKER_THREADS (2),
   1049         MHD_D_OPTION_DEFAULT_TIMEOUT_MILSEC (1500),
   1050         MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
   1051                                 0)))
   1052   {
   1053     fprintf (stderr,
   1054              "Failed to configure daemon!");
   1055     return 1;
   1056   }
   1057 
   1058   {
   1059     enum MHD_StatusCode sc;
   1060 
   1061     sc = MHD_daemon_start (d);
   1062     if (MHD_SC_OK != sc)
   1063     {
   1064 #ifdef FIXME_STATUS_CODE_TO_STRING_NOT_IMPLEMENTED
   1065       fprintf (stderr,
   1066                "Failed to start server: %s\n",
   1067                MHD_status_code_to_string_lazy (sc));
   1068 #else
   1069       fprintf (stderr,
   1070                "Failed to start server: %u\n",
   1071                (unsigned int) sc);
   1072 #endif
   1073       MHD_daemon_destroy (d);
   1074       return 1;
   1075     }
   1076   }
   1077 
   1078   {
   1079     union MHD_DaemonInfoFixedData info;
   1080     enum MHD_StatusCode sc;
   1081 
   1082     sc = MHD_daemon_get_info_fixed (
   1083       d,
   1084       MHD_DAEMON_INFO_FIXED_BIND_PORT,
   1085       &info);
   1086     if (MHD_SC_OK != sc)
   1087     {
   1088       fprintf (stderr,
   1089                "Failed to determine our port: %u\n",
   1090                (unsigned int) sc);
   1091       MHD_daemon_destroy (d);
   1092       return 1;
   1093     }
   1094     port = info.v_bind_port_uint16;
   1095   }
   1096 
   1097   {
   1098     int result;
   1099 
   1100     result = run_tests ();
   1101     MHD_daemon_destroy (d);
   1102     return result;
   1103   }
   1104 }