fix failure of gotwebd action_patch test on single-digit days of the month
[got-portable.git] / libexec / got-fetch-http / got-fetch-http.c
blobeb8ecc4066897d35b56e55f3dfc4bac2c6597358
1 /*
2 * Copyright (c) 2024 Tobias Heider <me@tobhe.de>
3 * Copyright (c) 2022 Omar Polo <op@openbsd.org>
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 #include "got_compat.h"
20 #include <sys/types.h>
21 #include <sys/queue.h>
22 #include <sys/socket.h>
24 #include <err.h>
25 #include <errno.h>
26 #include <limits.h>
27 #include <netdb.h>
28 #include <poll.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <tls.h>
33 #include <unistd.h>
35 #include "got_error.h"
36 #include "got_path.h"
37 #include "got_version.h"
39 #include "got_lib_pkt.h"
41 #include "bufio.h"
43 #define UPLOAD_PACK_ADV "application/x-git-upload-pack-advertisement"
44 #define UPLOAD_PACK_REQ "application/x-git-upload-pack-request"
45 #define UPLOAD_PACK_RES "application/x-git-upload-pack-result"
47 #define GOT_USERAGENT "got/" GOT_VERSION_STR
48 #define MINIMUM(a, b) ((a) < (b) ? (a) : (b))
49 #define hasprfx(str, p) (strncasecmp(str, p, strlen(p)) == 0)
51 FILE *tmp;
53 static int verbose;
55 static char *
56 bufio_getdelim_sync(struct bufio *bio, const char *nl, size_t *len)
58 int r;
60 do {
61 r = bufio_read(bio);
62 if (r == -1 && errno != EAGAIN)
63 errx(1, "bufio_read: %s", bufio_io_err(bio));
64 } while (r == -1 && errno == EAGAIN);
65 return buf_getdelim(&bio->rbuf, nl, len);
68 static size_t
69 bufio_drain_sync(struct bufio *bio, void *d, size_t len)
71 int r;
73 do {
74 r = bufio_read(bio);
75 if (r == -1 && errno != EAGAIN)
76 errx(1, "bufio_read: %s", bufio_io_err(bio));
77 } while (r == -1 && errno == EAGAIN);
78 return bufio_drain(bio, d, len);
81 static void
82 bufio_close_sync(struct bufio *bio)
84 int r;
86 do {
87 r = bufio_close(bio);
88 if (r == -1 && errno != EAGAIN)
89 errx(1, "bufio_close: %s", bufio_io_err(bio));
90 } while (r == -1 && errno == EAGAIN);
93 static long long
94 hexstrtonum(const char *str, long long min, long long max, const char **errstr)
96 long long lval;
97 char *cp;
99 errno = 0;
100 lval = strtoll(str, &cp, 16);
101 if (*str == '\0' || *cp != '\0') {
102 *errstr = "not a number";
103 return 0;
105 if ((errno == ERANGE && (lval == LONG_MAX || lval == LONG_MIN)) ||
106 lval < min || lval > max) {
107 *errstr = "out of range";
108 return 0;
111 *errstr = NULL;
112 return lval;
115 static int
116 dial(int https, const char *host, const char *port)
118 struct addrinfo hints, *res, *res0;
119 int error, saved_errno, fd = -1;
120 const char *cause = NULL;
122 memset(&hints, 0, sizeof(hints));
123 hints.ai_family = AF_UNSPEC;
124 hints.ai_socktype = SOCK_STREAM;
125 error = getaddrinfo(host, port, &hints, &res0);
126 if (error) {
127 warnx("%s", gai_strerror(error));
128 return -1;
131 for (res = res0; res; res = res->ai_next) {
132 fd = socket(res->ai_family, res->ai_socktype,
133 res->ai_protocol);
134 if (fd == -1) {
135 cause = "socket";
136 continue;
139 if (connect(fd, res->ai_addr, res->ai_addrlen) == 0)
140 break;
142 cause = "connect";
143 saved_errno = errno;
144 close(fd);
145 fd = -1;
146 errno = saved_errno;
148 freeaddrinfo(res0);
150 if (fd == -1) {
151 warn("%s", cause);
152 return -1;
155 return fd;
158 static int
159 http_open(struct bufio *bio, int https, const char *method, const char *host, const char *port,
160 const char *path, const char *path_sufx, const char *query, const char *ctype)
162 const char *chdr = NULL, *te = "";
163 char *p, *req;
164 int r;
166 if (strcmp(method, "POST") == 0)
167 te = "\r\nTransfer-Encoding: chunked\r\n";
169 if (ctype)
170 chdr = "Content-Type: ";
172 r = asprintf(&p, "%s%s/%s%s%s", got_path_is_absolute(path) ? "" :"/",
173 path, path_sufx, query ? "?" : "", query ? query : "");
174 if (r == -1)
175 err(1, "asprintf");
177 r = asprintf(&req, "%s %s HTTP/1.1\r\n"
178 "Host: %s\r\n"
179 "Connection: close\r\n"
180 "User-agent: %s\r\n"
181 "%s%s%s\r\n",
182 method, p, host, GOT_USERAGENT,
183 chdr ? chdr : "", ctype ? ctype : "", te);
184 if (r == -1)
185 err(1, "asprintf");
186 free(p);
188 if (verbose > 0)
189 fprintf(stderr, "%s: request: %s\n", getprogname(), req);
191 r = bufio_compose(bio, req, r);
192 if (r == -1)
193 err(1, "bufio_compose_fmt");
194 free(req);
196 do {
197 r = bufio_write(bio);
198 if (r == -1 && errno != EAGAIN)
199 errx(1, "bufio_write: %s", bufio_io_err(bio));
200 } while (bio->wbuf.len != 0);
202 return 0;
205 static int
206 http_parse_reply(struct bufio *bio, int *chunked, const char *expected_ctype)
208 char *cp, *line;
209 size_t linelen;
211 *chunked = 0;
213 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
214 if (line == NULL) {
215 warnx("%s: bufio_getdelim_sync()", __func__);
216 return -1;
219 if (verbose > 0)
220 fprintf(stderr, "%s: response: %s\n", getprogname(), line);
222 if ((cp = strchr(line, ' ')) == NULL) {
223 warnx("malformed HTTP response");
224 return -1;
226 cp++;
228 if (strncmp(cp, "200 ", 4) != 0) {
229 warnx("malformed HTTP response");
230 return -1;
232 buf_drain(&bio->rbuf, linelen);
234 while(1) {
235 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
236 if (line == NULL) {
237 warnx("%s: bufio_getdelim_sync()", __func__);
238 return -1;
240 if (*line == '\0') {
241 buf_drain(&bio->rbuf, linelen);
242 break;
245 if (hasprfx(line, "content-type:")) {
246 cp = strchr(line, ':') + 1;
247 cp += strspn(cp, " \t");
248 cp[strcspn(cp, " \t")] = '\0';
249 if (strcmp(cp, expected_ctype) != 0) {
250 warnx("server not using the \"smart\" "
251 "HTTP protocol.");
252 return -1;
255 if (hasprfx(line, "transfer-encoding:")) {
256 cp = strchr(line, ':') + 1;
257 cp += strspn(cp, " \t");
258 cp[strcspn(cp, " \t")] = '\0';
259 if (strcmp(cp, "chunked") != 0) {
260 warnx("unknown transfer-encoding");
261 return -1;
263 *chunked = 1;
265 buf_drain(&bio->rbuf, linelen);
268 return 0;
271 static ssize_t
272 http_read(struct bufio *bio, int chunked, size_t *chunksz, char *buf, size_t bufsz)
274 const char *errstr;
275 char *line = NULL;
276 size_t r;
277 ssize_t ret = 0, linelen;
279 if (!chunked)
280 return bufio_drain_sync(bio, buf, bufsz);
282 while (bufsz > 0) {
283 if (*chunksz == 0) {
284 again:
285 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
286 if (line == NULL) {
287 buf_drain(&bio->rbuf, linelen);
288 break;
290 if (*line == '\0') {
291 buf_drain(&bio->rbuf, linelen);
292 goto again; /* was the CRLF after the chunk */
295 *chunksz = hexstrtonum(line, 0, INT_MAX, &errstr);
296 if (errstr != NULL) {
297 warnx("invalid HTTP chunk: size is %s (%s)",
298 errstr, line);
299 ret = -1;
300 break;
303 if (*chunksz == 0) {
304 buf_drain(&bio->rbuf, linelen);
305 break;
307 buf_drain(&bio->rbuf, linelen);
310 r = bufio_drain_sync(bio, buf, MINIMUM(*chunksz, bufsz));
311 if (r == 0) {
312 break;
315 ret += r;
316 buf += r;
317 bufsz -= r;
318 *chunksz -= r;
321 return ret;
324 static int
325 http_chunk(struct bufio *bio, const void *buf, size_t len)
327 int r;
329 if (bufio_compose_fmt(bio, "%zx\r\n", len) == -1 ||
330 bufio_compose(bio, buf, len) == -1 ||
331 bufio_compose(bio, "\r\n", 2) == -1)
332 return 1;
334 do {
335 r = bufio_write(bio);
336 if (r == -1 && errno != EAGAIN)
337 errx(1, "bufio_read: %s", bufio_io_err(bio));
338 } while (bio->wbuf.len != 0);
340 return 0;
343 static int
344 get_refs(int https, const char *host, const char *port, const char *path)
346 struct bufio bio;
347 char buf[GOT_PKT_MAX];
348 const struct got_error *e;
349 size_t chunksz = 0;
350 ssize_t r;
351 int skip;
352 int chunked;
353 int sock;
354 int ret = -1;
356 if ((sock = dial(https, host, port)) == -1)
357 return -1;
359 if (bufio_init(&bio)) {
360 warnx("bufio_init");
361 goto err;
363 bufio_set_fd(&bio, sock);
364 if (https && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1) {
365 warnx("bufio_starttls");
366 goto err;
369 if (http_open(&bio, https, "GET", host, port, path, "info/refs",
370 "service=git-upload-pack", NULL) == -1)
371 goto err;
373 /* Fetch the initial reference announcement from the server. */
374 if (http_parse_reply(&bio, &chunked, UPLOAD_PACK_ADV) == -1)
375 goto err;
377 /* skip first pack; why git over http is like this? */
378 r = http_read(&bio, chunked, &chunksz, buf, 4);
379 if (r <= 0)
380 goto err;
382 e = got_pkt_readlen(&skip, buf, verbose);
383 if (e) {
384 warnx("%s", e->msg);
385 goto err;
388 /* TODO: validate it's # service=git-upload-pack\n */
389 while (skip > 0) {
390 r = http_read(&bio, chunked, &chunksz, buf,
391 MINIMUM(skip, sizeof(buf)));
392 if (r <= 0)
393 goto err;
394 skip -= r;
397 for (;;) {
398 r = http_read(&bio, chunked, &chunksz, buf, sizeof(buf));
399 if (r == -1)
400 goto err;
402 if (r == 0)
403 break;
405 fwrite(buf, 1, r, stdout);
408 fflush(stdout);
409 ret = 0;
410 err:
411 bufio_close_sync(&bio);
412 bufio_free(&bio);
413 return ret;
416 static int
417 upload_request(int https, const char *host, const char *port, const char *path,
418 FILE *in)
420 struct bufio bio;
421 char buf[GOT_PKT_MAX];
422 const struct got_error *e;
423 ssize_t r;
424 size_t chunksz = 0;
425 int t;
426 int chunked;
427 int sock;
428 int ret = -1;
430 if ((sock = dial(https, host, port)) == -1)
431 return -1;
433 if (bufio_init(&bio)) {
434 warnx("bufio_init");
435 goto err;
437 bufio_set_fd(&bio, sock);
438 if (https && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1) {
439 warnx("bufio_starttls");
440 goto err;
442 #ifndef PROFILE
443 /* TODO: can we push this upwards such that get_refs() is covered? */
444 if (pledge("stdio", NULL) == -1)
445 err(1, "pledge");
446 #endif
447 if (http_open(&bio, https, "POST", host, port, path, "git-upload-pack",
448 NULL, UPLOAD_PACK_REQ) == -1)
449 goto err;
452 * Read have/want lines generated by got-fetch-pack and forward
453 * them to the server in the POST request body.
455 for (;;) {
456 r = fread(buf, 1, 4, in);
457 if (r != 4)
458 goto err;
460 e = got_pkt_readlen(&t, buf, verbose);
461 if (e) {
462 warnx("%s", e->msg);
463 goto err;
466 if (t == 0) {
467 const char *flushpkt = "0000";
468 if (http_chunk(&bio, flushpkt, strlen(flushpkt)))
469 goto err;
470 continue; /* got-fetch-pack will send "done" */
473 if (t < 6) {
474 warnx("pktline len is too small");
475 goto err;
478 r = fread(buf + 4, 1, t - 4, in);
479 if (r != t - 4)
480 goto err;
482 if (http_chunk(&bio, buf, t))
483 goto err;
486 * Once got-fetch-pack is done the server will
487 * send pack file data.
489 if (t == 9 && strncmp(buf + 4, "done\n", 5) == 0) {
490 if (http_chunk(&bio, NULL, 0))
491 goto err;
492 break;
496 if (http_parse_reply(&bio, &chunked, UPLOAD_PACK_RES) == -1)
497 goto err;
499 /* Fetch pack file data from server. */
500 for (;;) {
501 r = http_read(&bio, chunked, &chunksz, buf, sizeof(buf));
502 if (r == -1)
503 goto err;
505 if (r == 0)
506 break;
508 fwrite(buf, 1, r, stdout);
511 ret = 0;
512 err:
513 bufio_close_sync(&bio);
514 bufio_free(&bio);
515 return ret;
518 static __dead void
519 usage(void)
521 fprintf(stderr, "usage: %s [-qv] proto host port path\n",
522 getprogname());
523 exit(1);
527 main(int argc, char **argv)
529 struct pollfd pfd;
530 const char *host, *port;
531 char *path;
532 int https = 0;
533 int ch;
535 #ifndef PROFILE
536 if (pledge("stdio rpath inet dns unveil", NULL) == -1)
537 err(1, "pledge");
538 #endif
540 while ((ch = getopt(argc, argv, "qv")) != -1) {
541 switch (ch) {
542 case 'q':
543 verbose = -1;
544 break;
545 case 'v':
546 verbose++;
547 break;
548 default:
549 usage();
552 argc -= optind;
553 argv += optind;
555 if (argc != 4)
556 usage();
558 https = strcmp(argv[0], "https") == 0;
559 #ifndef PROFILE
560 if (https) {
561 if (unveil("/etc/ssl/cert.pem", "r") == -1)
562 err(1, "unveil /etc/ssl/cert.pem");
563 } else {
564 /* drop "rpath" */
565 if (pledge("stdio inet dns unveil", NULL) == -1)
566 err(1, "pledge");
568 #else
569 if (unveil("gmon.out", "rwc") != 0)
570 err(1, "unveil gmon.out");
571 #endif
572 if (unveil(NULL, NULL) == -1)
573 err(1, "unveil NULL");
575 host = argv[1];
576 port = argv[2];
577 path = argv[3];
578 got_path_strip_trailing_slashes(path);
580 if (get_refs(https, host, port, path) == -1)
581 errx(1, "failed to get refs");
583 pfd.fd = 0;
584 pfd.events = POLLIN;
585 if (poll(&pfd, 1, INFTIM) == -1)
586 err(1, "poll");
588 if ((ch = fgetc(stdin)) == EOF)
589 return 0;
591 ungetc(ch, stdin);
592 if (upload_request(https, host, port, path, stdin) == -1) {
593 fflush(tmp);
594 errx(1, "failed to upload request");
597 return 0;