paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

stream_client.c (13190B)


      1 /*
      2   This file is part of paivana tests.
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   Paivana is free software; you can redistribute it and/or
      6   modify it under the terms of the GNU Affero General Public License
      7   as published by the Free Software Foundation; either version
      8   3, or (at your option) any later version.
      9 
     10   Paivana is distributed in the hope that it will be useful,
     11   but WITHOUT ANY WARRANTY; without even the implied warranty
     12   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
     13   the GNU Affero General Public License for more details.
     14 
     15   You should have received a copy of the GNU Affero General Public
     16   License along with Paivana; see the file COPYING.  If not,
     17   write to the Free Software Foundation, Inc., 51 Franklin
     18   Street, Fifth Floor, Boston, MA 02110-1301, USA.
     19 */
     20 
     21 /**
     22  * @file stream_client.c
     23  * @brief HTTP client for the streaming tests: verifies a body against
     24  *        the generated pattern as it arrives, and reports what it saw
     25  *        about the *framing* rather than only about the bytes.
     26  *
     27  *        Incremental verification is the point.  A 200 MiB body
     28  *        written to a file and compared afterwards costs the disk and
     29  *        the wall clock of two extra copies, and — worse for these
     30  *        tests — says nothing about whether paivana streamed it or
     31  *        assembled it first.  Here nothing larger than one libcurl
     32  *        delivery is ever held, so the client cannot be the thing that
     33  *        blows up the memory bound the tests are checking.
     34  *
     35  *        What it prints on stdout, one `key=value' per line:
     36  *
     37  *          status=N          HTTP status
     38  *          bytes=N           body bytes received
     39  *          pattern=ok|CORRUPT|...  incremental pattern check
     40  *          chunked=yes|no    Transfer-Encoding: chunked on the wire
     41  *          content_length=N|none   the client-visible Content-Length
     42  *          curl=N            CURLcode (0 = success, 18 = partial)
     43  *          ttfb_ms=N         time to first body byte
     44  *          total_ms=N        time to last byte
     45  *          body=...          the response body, with --print-body
     46  *
     47  *        The rate options are what make the congestion cases
     48  *        reproducible.  Without a slow peer at one end, loopback and
     49  *        the kernel socket buffers absorb everything and no
     50  *        backpressure is ever exercised: paivana would look
     51  *        well-behaved whether or not it had any.
     52  *
     53  *        The exit status is 0 whenever the exchange was carried out,
     54  *        *including* when the transfer failed: which failure occurred
     55  *        is data the driver asserts on, not a reason to abort. Exit 1
     56  *        is reserved for the client itself being unable to run.
     57  *
     58  *        Usage:
     59  *          stream_client URL [--upload N] [--chunked-upload]
     60  *                            [--expect-bytes N] [--head] [--print-body]
     61  *                            [--abort-after N] [--read-rate N]
     62  *                            [--upload-rate N] [--recv-buffer N]
     63  */
     64 #include "platform.h"
     65 #include <curl/curl.h>
     66 #include <inttypes.h>
     67 #include <stdbool.h>
     68 #include <stdint.h>
     69 #include <stdio.h>
     70 #include <stdlib.h>
     71 #include <string.h>
     72 #include <strings.h>
     73 
     74 
     75 /**
     76  * The body byte at offset @a off.  Must agree with the same function
     77  * in `stream_upstream.c'.
     78  */
     79 static uint8_t
     80 pattern_at (uint64_t off)
     81 {
     82   return (uint8_t) ((off * 7) + (off >> 13));
     83 }
     84 
     85 
     86 /**
     87  * Body bytes received so far.
     88  */
     89 static uint64_t received;
     90 
     91 /**
     92  * Offset of the first byte that did not match the pattern, or
     93  * UINT64_MAX if they all have.
     94  */
     95 static uint64_t bad_at = UINT64_MAX;
     96 
     97 /**
     98  * Did the response carry `Transfer-Encoding: chunked'?
     99  */
    100 static bool saw_chunked;
    101 
    102 /**
    103  * `Content-Length' of the response, or UINT64_MAX if there was none.
    104  */
    105 static uint64_t saw_length = UINT64_MAX;
    106 
    107 /**
    108  * Bytes of request body still to send.
    109  */
    110 static uint64_t upload_left;
    111 
    112 /**
    113  * Offset of the next request-body byte to send.
    114  */
    115 static uint64_t upload_off;
    116 
    117 /**
    118  * CURLOPT_BUFFERSIZE, or 0 to leave libcurl's default.  A small value
    119  * makes libcurl read the socket in small units, so paivana's content
    120  * reader is called far more often and for far less each time --
    121  * hundreds of pause/resume transitions where the default buffer would
    122  * produce a handful.
    123  */
    124 static unsigned long recv_buffer;
    125 
    126 /**
    127  * Read the response body at no more than this many bytes per second,
    128  * or 0 for as fast as it arrives.
    129  */
    130 static uint64_t read_rate;
    131 
    132 /**
    133  * Send the request body at no more than this many bytes per second,
    134  * or 0 for as fast as libcurl will take it.
    135  */
    136 static uint64_t upload_rate;
    137 
    138 /**
    139  * Abort the transfer once this many body bytes have arrived, or 0
    140  * never to.  For the cases about what paivana does when its client
    141  * walks away mid-response.
    142  */
    143 static uint64_t abort_after;
    144 
    145 /**
    146  * Print the response body rather than checking it against the
    147  * pattern?  For the targets whose answer is a short report about the
    148  * request we sent, which is the interesting half of an upload case.
    149  */
    150 static bool print_body;
    151 
    152 /**
    153  * First #BODY_KEEP bytes of the response, when @e print_body is set.
    154  */
    155 static char body_buf[512];
    156 
    157 /**
    158  * Bytes held in @e body_buf.
    159  */
    160 static size_t body_len;
    161 
    162 /**
    163  * Time of the first body byte, or 0 if none has arrived.
    164  */
    165 static struct timespec first_byte;
    166 
    167 /**
    168  * When the request started.
    169  */
    170 static struct timespec started;
    171 
    172 
    173 static long
    174 elapsed_ms (const struct timespec *from,
    175             const struct timespec *to)
    176 {
    177   return (long) ((to->tv_sec - from->tv_sec) * 1000
    178                  + (to->tv_nsec - from->tv_nsec) / 1000000);
    179 }
    180 
    181 
    182 /**
    183  * Sleep for @a ms milliseconds, resuming across signals.
    184  */
    185 static void
    186 sleep_ms (long ms)
    187 {
    188   struct timespec ts;
    189 
    190   if (0 >= ms)
    191     return;
    192   ts.tv_sec = ms / 1000;
    193   ts.tv_nsec = (ms % 1000) * 1000000L;
    194   while ( (0 != nanosleep (&ts, &ts)) &&
    195           (EINTR == errno) )
    196     ; /* again */
    197 }
    198 
    199 
    200 /**
    201  * Hold @a moved bytes down to @a rate bytes per second, measuring from
    202  * @a since.
    203  *
    204  * Paced against the clock rather than by a fixed nap per callback: the
    205  * callback is handed whatever libcurl happened to read, so a fixed nap
    206  * would make the actual rate depend on the delivery size and the cases
    207  * that assert on elapsed time would be measuring the wrong thing.
    208  *
    209  * @param since when the transfer started
    210  * @param moved bytes moved so far
    211  * @param rate bytes per second to hold to
    212  */
    213 static void
    214 pace (const struct timespec *since,
    215       uint64_t moved,
    216       uint64_t rate)
    217 {
    218   struct timespec now;
    219   long due_ms;
    220   long spent_ms;
    221 
    222   if (0 == rate)
    223     return;
    224   clock_gettime (CLOCK_MONOTONIC,
    225                  &now);
    226   due_ms = (long) ((moved * 1000ULL) / rate);
    227   spent_ms = elapsed_ms (since,
    228                          &now);
    229   sleep_ms (due_ms - spent_ms);
    230 }
    231 
    232 
    233 static size_t
    234 write_cb (char *ptr,
    235           size_t size,
    236           size_t nmemb,
    237           void *cls)
    238 {
    239   size_t n = size * nmemb;
    240 
    241   (void) cls;
    242   if (0 == received)
    243     clock_gettime (CLOCK_MONOTONIC,
    244                    &first_byte);
    245   if (print_body)
    246   {
    247     size_t room = sizeof (body_buf) - body_len;
    248     size_t take = (n < room) ? n : room;
    249 
    250     memcpy (&body_buf[body_len],
    251             ptr,
    252             take);
    253     body_len += take;
    254   }
    255   else
    256   {
    257     for (size_t i = 0; i < n; i++)
    258       if ( (UINT64_MAX == bad_at) &&
    259            ((uint8_t) ptr[i] != pattern_at (received + i)) )
    260         bad_at = received + i;
    261   }
    262   received += n;
    263   if ( (0 != abort_after) &&
    264        (received >= abort_after) )
    265     return 0; /* fail the transfer: this is us hanging up */
    266   /* Blocking here is the whole point: libcurl stops reading the socket
    267      while we sleep, its receive window closes, and paivana's own
    268      download ring backs up.  That is the congestion the cases are
    269      about, and there is no way to produce it on loopback without it. */
    270   pace (&first_byte,
    271         received,
    272         read_rate);
    273   return n;
    274 }
    275 
    276 
    277 static size_t
    278 header_cb (char *ptr,
    279            size_t size,
    280            size_t nmemb,
    281            void *cls)
    282 {
    283   size_t n = size * nmemb;
    284   static const char te[] = "transfer-encoding:";
    285   static const char cl[] = "content-length:";
    286 
    287   (void) cls;
    288   if ( (n >= sizeof (te) - 1) &&
    289        (0 == strncasecmp (ptr, te, sizeof (te) - 1)) )
    290   {
    291     /* The whole point of looking is to see the framing paivana chose,
    292        so match the value rather than assume it. */
    293     for (size_t i = sizeof (te) - 1; i + 6 < n; i++)
    294       if (0 == strncasecmp (&ptr[i], "chunked", 7))
    295       {
    296         saw_chunked = true;
    297         break;
    298       }
    299   }
    300   if ( (n >= sizeof (cl) - 1) &&
    301        (0 == strncasecmp (ptr, cl, sizeof (cl) - 1)) )
    302     saw_length = strtoull (&ptr[sizeof (cl) - 1],
    303                            NULL,
    304                            10);
    305   return n;
    306 }
    307 
    308 
    309 static size_t
    310 read_cb (char *ptr,
    311          size_t size,
    312          size_t nmemb,
    313          void *cls)
    314 {
    315   size_t room = size * nmemb;
    316   size_t n;
    317 
    318   (void) cls;
    319   n = (size_t) ((upload_left < (uint64_t) room)
    320                 ? upload_left
    321                 : (uint64_t) room);
    322   for (size_t i = 0; i < n; i++)
    323     ptr[i] = (char) pattern_at (upload_off + i);
    324   upload_off += n;
    325   upload_left -= n;
    326   pace (&started,
    327         upload_off,
    328         upload_rate);
    329   return n;
    330 }
    331 
    332 
    333 int
    334 main (int argc,
    335       char **argv)
    336 {
    337   CURL *eh;
    338   CURLcode rc;
    339   long status = 0;
    340   const char *url = NULL;
    341   uint64_t upload = 0;
    342   bool chunked_upload = false;
    343   bool head = false;
    344   uint64_t expect = UINT64_MAX;
    345   struct timespec done;
    346   struct curl_slist *hdrs = NULL;
    347 
    348   for (int i = 1; i < argc; i++)
    349   {
    350     if (0 == strcmp (argv[i], "--upload"))
    351       upload = strtoull (argv[++i], NULL, 10);
    352     else if (0 == strcmp (argv[i], "--chunked-upload"))
    353       chunked_upload = true;
    354     else if (0 == strcmp (argv[i], "--head"))
    355       head = true;
    356     else if (0 == strcmp (argv[i], "--print-body"))
    357       print_body = true;
    358     else if (0 == strcmp (argv[i], "--abort-after"))
    359       abort_after = strtoull (argv[++i], NULL, 10);
    360     else if (0 == strcmp (argv[i], "--read-rate"))
    361       read_rate = strtoull (argv[++i], NULL, 10);
    362     else if (0 == strcmp (argv[i], "--upload-rate"))
    363       upload_rate = strtoull (argv[++i], NULL, 10);
    364     else if (0 == strcmp (argv[i], "--recv-buffer"))
    365       recv_buffer = strtoul (argv[++i], NULL, 10);
    366     else if (0 == strcmp (argv[i], "--expect-bytes"))
    367       expect = strtoull (argv[++i], NULL, 10);
    368     else if (NULL == url)
    369       url = argv[i];
    370     else
    371     {
    372       fprintf (stderr,
    373                "unexpected argument `%s'\n",
    374                argv[i]);
    375       return 1;
    376     }
    377   }
    378   if (NULL == url)
    379   {
    380     fprintf (stderr,
    381              "usage: %s URL [--upload N] [--chunked-upload]"
    382              " [--head] [--print-body] [--expect-bytes N]"
    383              " [--abort-after N] [--read-rate N] [--upload-rate N]"
    384              " [--recv-buffer N]\n",
    385              argv[0]);
    386     return 1;
    387   }
    388   eh = curl_easy_init ();
    389   if (NULL == eh)
    390   {
    391     fprintf (stderr,
    392              "curl_easy_init failed\n");
    393     return 1;
    394   }
    395   upload_left = upload;
    396   curl_easy_setopt (eh, CURLOPT_URL, url);
    397   curl_easy_setopt (eh, CURLOPT_WRITEFUNCTION, &write_cb);
    398   curl_easy_setopt (eh, CURLOPT_HEADERFUNCTION, &header_cb);
    399   curl_easy_setopt (eh, CURLOPT_NOSIGNAL, 1L);
    400   if (0 != recv_buffer)
    401     curl_easy_setopt (eh,
    402                       CURLOPT_BUFFERSIZE,
    403                       (long) recv_buffer);
    404   if (head)
    405   {
    406     curl_easy_setopt (eh, CURLOPT_NOBODY, 1L);
    407   }
    408   else if (0 != upload)
    409   {
    410     curl_easy_setopt (eh, CURLOPT_POST, 1L);
    411     curl_easy_setopt (eh, CURLOPT_READFUNCTION, &read_cb);
    412     if (chunked_upload)
    413     {
    414       /* Say nothing about the length and libcurl chunks; this is the
    415          framing paivana has to reproduce upstream. */
    416       hdrs = curl_slist_append (hdrs,
    417                                 "Transfer-Encoding: chunked");
    418       curl_easy_setopt (eh, CURLOPT_HTTPHEADER, hdrs);
    419     }
    420     else
    421     {
    422       curl_easy_setopt (eh,
    423                         CURLOPT_POSTFIELDSIZE_LARGE,
    424                         (curl_off_t) upload);
    425     }
    426   }
    427   clock_gettime (CLOCK_MONOTONIC, &started);
    428   rc = curl_easy_perform (eh);
    429   clock_gettime (CLOCK_MONOTONIC, &done);
    430   curl_easy_getinfo (eh,
    431                      CURLINFO_RESPONSE_CODE,
    432                      &status);
    433   printf ("status=%ld\n", status);
    434   printf ("bytes=%" PRIu64 "\n", received);
    435   if (print_body)
    436     printf ("pattern=n/a\n");
    437   else if (UINT64_MAX != bad_at)
    438     printf ("pattern=CORRUPT at %" PRIu64 "\n", bad_at);
    439   else if ( (UINT64_MAX != expect) &&
    440             (received != expect) )
    441     printf ("pattern=SHORT want %" PRIu64 "\n", expect);
    442   else
    443     printf ("pattern=ok\n");
    444   printf ("chunked=%s\n", saw_chunked ? "yes" : "no");
    445   if (UINT64_MAX == saw_length)
    446     printf ("content_length=none\n");
    447   else
    448     printf ("content_length=%" PRIu64 "\n", saw_length);
    449   printf ("curl=%d\n", (int) rc);
    450   printf ("ttfb_ms=%ld\n",
    451           (0 == received) ? -1 : elapsed_ms (&started, &first_byte));
    452   printf ("total_ms=%ld\n", elapsed_ms (&started, &done));
    453   if (print_body)
    454     printf ("body=%.*s\n",
    455             (int) body_len,
    456             body_buf);
    457   curl_slist_free_all (hdrs);
    458   curl_easy_cleanup (eh);
    459   return 0;
    460 }
    461 
    462 
    463 /* end of stream_client.c */