paivana

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

pipeline_client.c (9538B)


      1 /*
      2   This file is part of Paivana.
      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, but
     11   WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13   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 pipeline_client.c
     23  * @brief HTTP/1.1 pipelining client: opens a single TCP connection
     24  *        to a host/port, sends N GET requests back-to-back *without*
     25  *        waiting for their responses, then reads N responses in
     26  *        order and prints each body (separated by "---").
     27  *
     28  * Used by the paivana test suite to verify that paivana correctly
     29  * handles pipelined requests on a single keep-alive connection.
     30  */
     31 #include <arpa/inet.h>
     32 #include <errno.h>
     33 #include <netdb.h>
     34 #include <netinet/in.h>
     35 #include <stdio.h>
     36 #include <stdlib.h>
     37 #include <string.h>
     38 #include <sys/socket.h>
     39 #include <sys/types.h>
     40 #include <unistd.h>
     41 
     42 #define BUF_GROW 8192
     43 
     44 static int
     45 connect_to (const char *host,
     46             const char *port)
     47 {
     48   struct addrinfo hints = { 0 };
     49   struct addrinfo *res = NULL;
     50   int rc;
     51 
     52   hints.ai_family = AF_UNSPEC;
     53   hints.ai_socktype = SOCK_STREAM;
     54   rc = getaddrinfo (host,
     55                     port,
     56                     &hints,
     57                     &res);
     58   if (0 != rc)
     59   {
     60     fprintf (stderr,
     61              "getaddrinfo(%s:%s): %s\n",
     62              host,
     63              port,
     64              gai_strerror (rc));
     65     return -1;
     66   }
     67   for (struct addrinfo *p = res; NULL != p; p = p->ai_next)
     68   {
     69     int fd = socket (p->ai_family,
     70                      p->ai_socktype,
     71                      p->ai_protocol);
     72     if (fd < 0)
     73       continue;
     74     if (0 == connect (fd,
     75                       p->ai_addr,
     76                       p->ai_addrlen))
     77     {
     78       freeaddrinfo (res);
     79       return fd;
     80     }
     81     close (fd);
     82   }
     83   freeaddrinfo (res);
     84   fprintf (stderr,
     85            "could not connect to %s:%s\n",
     86            host,
     87            port);
     88   return -1;
     89 }
     90 
     91 
     92 static int
     93 write_all (int fd,
     94            const char *buf,
     95            size_t len)
     96 {
     97   while (len > 0)
     98   {
     99     ssize_t w = write (fd,
    100                        buf,
    101                        len);
    102 
    103     if (w < 0)
    104     {
    105       if (EINTR == errno)
    106         continue;
    107       return -1;
    108     }
    109     buf += w;
    110     len -= (size_t) w;
    111   }
    112   return 0;
    113 }
    114 
    115 
    116 /**
    117  * Read exactly @a want bytes from @a fd into the tail of the buffer.
    118  * Grows the buffer as needed.  @a have is updated.
    119  */
    120 static int
    121 ensure_bytes (int fd,
    122               char **buf,
    123               size_t *cap,
    124               size_t *have,
    125               size_t want)
    126 {
    127   while (*have < want)
    128   {
    129     ssize_t r;
    130     if (*have + BUF_GROW > *cap)
    131     {
    132       size_t nc = *cap ? *cap * 2 : BUF_GROW;
    133       char *nb;
    134 
    135       while (nc < *have + BUF_GROW)
    136         nc *= 2;
    137       nb = realloc (*buf,
    138                     nc);
    139       if (NULL == nb)
    140         return -1;
    141       *buf = nb;
    142       *cap = nc;
    143     }
    144 
    145     r = read (fd,
    146               *buf + *have,
    147               *cap - *have);
    148     if (r < 0)
    149     {
    150       if (EINTR == errno)
    151         continue;
    152       return -1;
    153     }
    154     if (0 == r)
    155       return -1; /* EOF */
    156     *have += (size_t) r;
    157   }
    158   return 0;
    159 }
    160 
    161 
    162 /**
    163  * Find the end of the HTTP header block ("\r\n\r\n") starting at
    164  * some offset in buf.  Returns the absolute offset of the byte
    165  * just past the final "\r\n\r\n", or (size_t) -1 if not yet present.
    166  * Grows the buffer / reads more data as needed.
    167  */
    168 static size_t
    169 read_headers (int fd,
    170               char **buf,
    171               size_t *cap,
    172               size_t *have,
    173               size_t start)
    174 {
    175   /* First offset not yet examined.  Carried across the read loop:
    176      restarting the scan at @a start after every read would be
    177      quadratic in the size of the header block. */
    178   size_t scan = start;
    179 
    180   while (1)
    181   {
    182     while (scan + 3 < *have)
    183     {
    184       if ( ('\r' == (*buf)[scan]) &&
    185            ('\n' == (*buf)[scan + 1]) &&
    186            ('\r' == (*buf)[scan + 2]) &&
    187            ('\n' == (*buf)[scan + 3]) )
    188         return scan + 4;
    189       scan++;
    190     }
    191     if (ensure_bytes (fd,
    192                       buf,
    193                       cap,
    194                       have,
    195                       *have + 1))
    196       return (size_t) -1;
    197   }
    198 }
    199 
    200 
    201 /**
    202  * Extract Content-Length from a header block [start, hdr_end).
    203  * Returns -1 if not found.
    204  */
    205 static long long
    206 header_content_length (const char *buf,
    207                        size_t start,
    208                        size_t hdr_end)
    209 {
    210   const char *hdrs = buf + start;
    211   size_t len = hdr_end - start;
    212   const char *p = hdrs;
    213   const char *end = hdrs + len;
    214   const char *needle = "Content-Length:";
    215 
    216   while (p + strlen (needle) < end)
    217   {
    218     const char *line_end = memchr (p,
    219                                    '\n',
    220                                    end - p);
    221 
    222     if (NULL == line_end)
    223       break;
    224     if (0 == strncasecmp (p,
    225                           needle,
    226                           strlen (needle)))
    227     {
    228       const char *v = p + strlen (needle);
    229 
    230       while ( (v < line_end) &&
    231               (' ' == *v ||
    232                '\t' == *v) )
    233         v++;
    234       return strtoll (v,
    235                       NULL,
    236                       10);
    237     }
    238     p = line_end + 1;
    239   }
    240   return -1;
    241 }
    242 
    243 
    244 /**
    245  * Parse the status code out of "HTTP/1.1 <code> ..."
    246  */
    247 static int
    248 header_status (const char *buf,
    249                size_t start,
    250                size_t hdr_end)
    251 {
    252   const char *p = buf + start;
    253   const char *sp = memchr (p,
    254                            ' ',
    255                            hdr_end - start);
    256 
    257   if (NULL == sp)
    258     return -1;
    259   return atoi (sp + 1);
    260 }
    261 
    262 
    263 int
    264 main (int argc, char **argv)
    265 {
    266   const char *host;
    267   const char *port;
    268   int n_paths;
    269   int fd;
    270 
    271   /* Before argc is checked: with argc == 1, argv[2] is a read one
    272      past the end of the argument vector. */
    273   if (argc < 4)
    274   {
    275     fprintf (stderr,
    276              "usage: %s <host> <port> <path> [<path>...]\n",
    277              argv[0]);
    278     return 2;
    279   }
    280   host = argv[1];
    281   port = argv[2];
    282   n_paths = argc - 3;
    283 
    284   fd = connect_to (host,
    285                    port);
    286   if (fd < 0)
    287     return 1;
    288 
    289   /* Pipeline: send all requests back-to-back. */
    290   for (int i = 0; i < n_paths; i++)
    291   {
    292     char req[2048];
    293     const char *conn_hdr
    294       = (i == n_paths - 1)
    295       ? "close"
    296       : "keep-alive";
    297     int n = snprintf (req,
    298                       sizeof (req),
    299                       "GET %s HTTP/1.1\r\n"
    300                       "Host: %s:%s\r\n"
    301                       "User-Agent: paivana-pipeline-test\r\n"
    302                       "Connection: %s\r\n"
    303                       "\r\n",
    304                       argv[3 + i],
    305                       host,
    306                       port,
    307                       conn_hdr);
    308     if ( (n < 0) ||
    309          ((size_t) n >= sizeof (req)) )
    310     {
    311       fprintf (stderr,
    312                "request too long\n");
    313       close (fd);
    314       return 1;
    315     }
    316     if (0 != write_all (fd,
    317                         req,
    318                         (size_t) n))
    319     {
    320       fprintf (stderr,
    321                "write failed: %s\n",
    322                strerror (errno));
    323       close (fd);
    324       return 1;
    325     }
    326   }
    327 
    328   {
    329     /* Now read N responses in order, using Content-Length. */
    330     char *buf = NULL;
    331     size_t cap = 0;
    332     size_t have = 0;
    333     size_t pos = 0;
    334     int ret = 0;
    335 
    336     for (int i = 0; i < n_paths; i++)
    337     {
    338       size_t hdr_end = read_headers (fd,
    339                                      &buf,
    340                                      &cap,
    341                                      &have,
    342                                      pos);
    343       long status;
    344       long long cl;
    345       size_t body_end;
    346 
    347       if ((size_t) -1 == hdr_end)
    348       {
    349         fprintf (stderr,
    350                  "failed to read headers of response #%d\n",
    351                  i);
    352         ret = 1;
    353         break;
    354       }
    355       status = header_status (buf,
    356                               pos,
    357                               hdr_end);
    358       cl = header_content_length (buf,
    359                                   pos,
    360                                   hdr_end);
    361       if (cl < 0)
    362       {
    363         fprintf (stderr,
    364                  "response #%d has no Content-Length (got status %d); "
    365                  "cannot safely parse pipelined response stream\n",
    366                  i,
    367                  (int) status);
    368         ret = 1;
    369         break;
    370       }
    371 
    372       body_end = hdr_end + (size_t) cl;
    373       if (0 != ensure_bytes (fd,
    374                              &buf,
    375                              &cap,
    376                              &have,
    377                              body_end))
    378       {
    379         fprintf (stderr,
    380                  "short body for response #%d (expected %lld bytes)\n",
    381                  i, cl);
    382         ret = 1;
    383         break;
    384       }
    385       printf ("--- response %d: status=%d len=%lld ---\n",
    386               i,
    387               (int) status,
    388               cl);
    389       fwrite (buf + hdr_end,
    390               1,
    391               (size_t) cl,
    392               stdout);
    393       if ( (cl > 0) &&
    394            (buf[body_end - 1] != '\n') )
    395         putchar ('\n');
    396       pos = body_end;
    397     }
    398     free (buf);
    399     close (fd);
    400     return ret;
    401   }
    402 }