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 }