Blob


1 /*
2 * Copyright (c) 2024 Tobias Heider <me@tobhe.de>
3 * Copyright (c) 2022 Omar Polo <op@openbsd.org>
4 *
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.
8 *
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.
16 */
18 #include <sys/types.h>
19 #include <sys/socket.h>
21 #include <err.h>
22 #include <errno.h>
23 #include <limits.h>
24 #include <netdb.h>
25 #include <poll.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <tls.h>
30 #include <unistd.h>
32 #include "got_version.h"
34 #include "got_lib_pkt.h"
36 #include "bufio.h"
38 #define UPLOAD_PACK_ADV "application/x-git-upload-pack-advertisement"
39 #define UPLOAD_PACK_REQ "application/x-git-upload-pack-request"
40 #define UPLOAD_PACK_RES "application/x-git-upload-pack-result"
42 #define GOT_USERAGENT "got/" GOT_VERSION_STR
43 #define MINIMUM(a, b) ((a) < (b) ? (a) : (b))
44 #define hasprfx(str, p) (strncasecmp(str, p, strlen(p)) == 0)
46 FILE *tmp;
48 static int verbose;
50 static char *
51 bufio_getdelim_sync(struct bufio *bio, const char *nl, size_t *len)
52 {
53 int r;
55 do {
56 r = bufio_read(bio);
57 if (r == -1 && errno != EAGAIN)
58 errx(1, "bufio_read: %s", bufio_io_err(bio));
59 } while (r == -1 && errno == EAGAIN);
60 return buf_getdelim(&bio->rbuf, nl, len);
61 }
63 static size_t
64 bufio_drain_sync(struct bufio *bio, void *d, size_t len)
65 {
66 int r;
68 do {
69 r = bufio_read(bio);
70 if (r == -1 && errno != EAGAIN)
71 errx(1, "bufio_read: %s", bufio_io_err(bio));
72 } while (r == -1 && errno == EAGAIN);
73 return bufio_drain(bio, d, len);
74 }
76 static void
77 bufio_close_sync(struct bufio *bio)
78 {
79 int r;
81 do {
82 r = bufio_close(bio);
83 if (r == -1 && errno == EAGAIN)
84 errx(1, "bufio_read: %s", bufio_io_err(bio));
85 } while (r == -1 && errno == EAGAIN);
86 }
88 static long long
89 hexstrtonum(const char *str, long long min, long long max, const char **errstr)
90 {
91 long long lval;
92 char *cp;
94 errno = 0;
95 lval = strtoll(str, &cp, 16);
96 if (*str == '\0' || *cp != '\0') {
97 *errstr = "not a number";
98 return 0;
99 }
100 if ((errno == ERANGE && (lval == LONG_MAX || lval == LONG_MIN)) ||
101 lval < min || lval > max) {
102 *errstr = "out of range";
103 return 0;
106 *errstr = NULL;
107 return lval;
110 static int
111 dial(int https, const char *host, const char *port)
113 struct addrinfo hints, *res, *res0;
114 int error, saved_errno, fd = -1;
115 const char *cause = NULL;
117 memset(&hints, 0, sizeof(hints));
118 hints.ai_family = AF_UNSPEC;
119 hints.ai_socktype = SOCK_STREAM;
120 error = getaddrinfo(host, port, &hints, &res0);
121 if (error) {
122 warnx("%s", gai_strerror(error));
123 return -1;
126 for (res = res0; res; res = res->ai_next) {
127 fd = socket(res->ai_family, res->ai_socktype,
128 res->ai_protocol);
129 if (fd == -1) {
130 cause = "socket";
131 continue;
134 if (connect(fd, res->ai_addr, res->ai_addrlen) == 0)
135 break;
137 cause = "connect";
138 saved_errno = errno;
139 close(fd);
140 fd = -1;
141 errno = saved_errno;
143 freeaddrinfo(res0);
145 if (fd == -1) {
146 warn("%s", cause);
147 return -1;
150 return fd;
153 static int
154 http_open(struct bufio *bio, int https, const char *method, const char *host, const char *port,
155 const char *path, const char *path_sufx, const char *query, const char *ctype)
157 const char *chdr = NULL, *te = "";
158 char *p, *req;
159 int r;
161 if (path_sufx != NULL && *path && path[strlen(path) - 1] == '/')
162 path_sufx++; /* skip the slash */
164 if (strcmp(method, "POST") == 0)
165 te = "\r\nTransfer-Encoding: chunked\r\n";
167 if (ctype)
168 chdr = "Content-Type: ";
170 r = asprintf(&p, "%s/%s%s%s", path, path_sufx,
171 query ? "?" : "", query ? query : "");
172 if (r == -1)
173 err(1, "asprintf");
175 r = asprintf(&req, "%s %s HTTP/1.1\r\n"
176 "Host: %s\r\n"
177 "Connection: close\r\n"
178 "User-agent: %s\r\n"
179 "%s%s%s\r\n",
180 method, p, host, GOT_USERAGENT,
181 chdr ? chdr : "", ctype ? ctype : "", te);
182 if (r == -1)
183 err(1, "asprintf");
184 free(p);
186 if (verbose > 0)
187 fprintf(stderr, "%s: request: %s\n", getprogname(), req);
190 r = bufio_compose(bio, req, r);
191 if (r == -1)
192 err(1, "bufio_compose_fmt");
193 free(req);
195 do {
196 r = bufio_write(bio);
197 if (r == -1 && errno != EAGAIN)
198 errx(1, "bufio_read: %s", bufio_io_err(bio));
199 } while (bio->wbuf.len != 0);
201 return 0;
204 static int
205 http_parse_reply(struct bufio *bio, int *chunked, const char *expected_ctype)
207 char *cp, *line;
208 size_t linelen;
210 *chunked = 0;
212 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
213 if (line == NULL) {
214 warnx("%s: bufio_getdelim_sync()", __func__);
215 return -1;
218 if (verbose > 0)
219 fprintf(stderr, "%s: response: %s\n", getprogname(), line);
221 if ((cp = strchr(line, ' ')) == NULL) {
222 warnx("malformed HTTP response");
223 return -1;
225 cp++;
227 if (strncmp(cp, "200 ", 4) != 0) {
228 warnx("malformed HTTP response");
229 return -1;
231 buf_drain(&bio->rbuf, linelen);
233 while(1) {
234 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
235 if (line == NULL) {
236 warnx("%s: bufio_getdelim_sync()", __func__);
237 return -1;
239 if (*line == '\0') {
240 buf_drain(&bio->rbuf, linelen);
241 break;
244 if (hasprfx(line, "content-type:")) {
245 cp = strchr(line, ':') + 1;
246 cp += strspn(cp, " \t");
247 cp[strcspn(cp, " \t")] = '\0';
248 if (strcmp(cp, expected_ctype) != 0) {
249 warnx("server not using the \"smart\" "
250 "HTTP protocol.");
251 return -1;
254 if (hasprfx(line, "transfer-encoding:")) {
255 cp = strchr(line, ':') + 1;
256 cp += strspn(cp, " \t");
257 cp[strcspn(cp, " \t")] = '\0';
258 if (strcmp(cp, "chunked") != 0) {
259 warnx("unknown transfer-encoding");
260 return -1;
262 *chunked = 1;
264 buf_drain(&bio->rbuf, linelen);
267 return 0;
270 static ssize_t
271 http_read(struct bufio *bio, int chunked, size_t *chunksz, char *buf, size_t bufsz)
273 const char *errstr;
274 char *line = NULL;
275 size_t r;
276 ssize_t ret = 0, linelen;
278 if (!chunked) {
279 r = bufio_drain_sync(bio, buf, bufsz);
280 if (r == 0)
281 return -1;
282 return r;
285 while (bufsz > 0) {
286 if (*chunksz == 0) {
287 again:
288 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
289 if (line == NULL) {
290 buf_drain(&bio->rbuf, linelen);
291 break;
293 if (*line == '\0') {
294 buf_drain(&bio->rbuf, linelen);
295 goto again; /* was the CRLF after the chunk */
298 *chunksz = hexstrtonum(line, 0, INT_MAX, &errstr);
299 if (errstr != NULL) {
300 warnx("invalid HTTP chunk: size is %s (%s)",
301 errstr, line);
302 ret = -1;
303 break;
306 if (*chunksz == 0) {
307 buf_drain(&bio->rbuf, linelen);
308 break;
310 buf_drain(&bio->rbuf, linelen);
313 r = bufio_drain_sync(bio, buf, MINIMUM(*chunksz, bufsz));
314 if (r == 0) {
315 break;
318 ret += r;
319 buf += r;
320 bufsz -= r;
321 *chunksz -= r;
324 return ret;
327 static int
328 http_chunk(struct bufio *bio, const void *buf, size_t len)
330 int r;
332 if (bufio_compose_fmt(bio, "%zx\r\n", len) ||
333 bufio_compose(bio, buf, len) ||
334 bufio_compose(bio, "\r\n", 2))
335 return 1;
337 do {
338 r = bufio_write(bio);
339 if (r == -1 && errno != EAGAIN)
340 errx(1, "bufio_read: %s", bufio_io_err(bio));
341 } while (bio->wbuf.len != 0);
343 return 0;
346 static int
347 get_refs(int https, const char *host, const char *port, const char *path)
349 struct bufio bio;
350 char buf[GOT_PKT_MAX];
351 const char *errstr, *sufx = "/info/refs";
352 size_t skip, chunksz = 0;
353 ssize_t r;
354 int chunked;
355 int sock;
356 int ret = -1;
358 if ((sock = dial(https, host, port)) == -1)
359 return -1;
361 if (bufio_init(&bio)) {
362 warnx("bufio_init");
363 goto err;
365 bufio_set_fd(&bio, sock);
366 if (https && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1) {
367 warnx("bufio_starttls");
368 goto err;
371 if (http_open(&bio, https, "GET", host, port, path, sufx,
372 "service=git-upload-pack", NULL) == -1)
373 goto err;
375 /* Fetch the initial reference announcement from the server. */
376 if (http_parse_reply(&bio, &chunked, UPLOAD_PACK_ADV) == -1)
377 goto err;
379 /* skip first pack; why git over http is like this? */
380 r = http_read(&bio, chunked, &chunksz, buf, 4);
381 if (r <= 0)
382 goto err;
383 buf[4] = '\0';
384 skip = hexstrtonum(buf, 0, INT_MAX, &errstr);
385 if (errstr != NULL) {
386 warnx("pktlen is %s", errstr);
387 goto err;
390 /* TODO: validate it's # service=git-upload-pack\n */
391 while (skip > 0) {
392 r = http_read(&bio, chunked, &chunksz, buf,
393 MINIMUM(skip, sizeof(buf)));
394 if (r <= 0)
395 goto err;
396 skip -= r;
399 for (;;) {
400 r = http_read(&bio, chunked, &chunksz, buf, sizeof(buf));
401 if (r == -1)
402 goto err;
404 if (r == 0)
405 break;
407 fwrite(buf, 1, r, stdout);
410 fflush(stdout);
411 ret = 0;
412 err:
413 bufio_close_sync(&bio);
414 bufio_free(&bio);
415 return ret;
418 static int
419 upload_request(int https, const char *host, const char *port, const char *path,
420 FILE *in)
422 struct bufio bio;
423 const char *errstr;
424 char buf[GOT_PKT_MAX];
425 ssize_t r;
426 size_t chunksz = 0;
427 long long t;
428 int chunked;
429 int sock;
430 int ret = -1;
432 if ((sock = dial(https, host, port)) == -1)
433 return -1;
435 if (bufio_init(&bio)) {
436 warnx("bufio_init");
437 goto err;
439 bufio_set_fd(&bio, sock);
440 if (https && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1) {
441 warnx("bufio_starttls");
442 goto err;
444 #ifndef PROFILE
445 /* TODO: can we push this upwards such that get_refs() is covered? */
446 if (pledge("stdio", NULL) == -1)
447 err(1, "pledge");
448 #endif
449 if (http_open(&bio, https, "POST", host, port, path, "/git-upload-pack",
450 NULL, UPLOAD_PACK_REQ) == -1)
451 goto err;
453 /*
454 * Read have/want lines generated by got-fetch-pack and forward
455 * them to the server in the POST request body.
456 */
457 for (;;) {
458 r = fread(buf, 1, 4, in);
459 if (r != 4)
460 goto err;
462 buf[4] = '\0';
463 t = hexstrtonum(buf, 0, sizeof(buf), &errstr);
464 if (errstr != NULL) {
465 warnx("pktline len is %s", errstr);
466 goto err;
469 if (t == 0) {
470 const char *flushpkt = "0000";
471 if (http_chunk(&bio, flushpkt, strlen(flushpkt)))
472 goto err;
473 continue; /* got-fetch-pack will send "done" */
476 if (t < 6) {
477 warnx("pktline len is too small");
478 goto err;
481 r = fread(buf + 4, 1, t - 4, in);
482 if (r != t - 4)
483 goto err;
485 if (http_chunk(&bio, buf, t))
486 goto err;
488 /*
489 * Once got-fetch-pack is done the server will
490 * send pack file data.
491 */
492 if (t == 9 && strncmp(buf + 4, "done\n", 5) == 0) {
493 if (http_chunk(&bio, NULL, 0))
494 goto err;
495 break;
499 if (http_parse_reply(&bio, &chunked, UPLOAD_PACK_RES) == -1)
500 goto err;
502 /* Fetch pack file data from server. */
503 for (;;) {
504 r = http_read(&bio, chunked, &chunksz, buf, sizeof(buf));
505 if (r == -1)
506 goto err;
508 if (r == 0)
509 break;
511 fwrite(buf, 1, r, stdout);
514 ret = 0;
515 err:
516 bufio_close_sync(&bio);
517 bufio_free(&bio);
518 return ret;
521 static __dead void
522 usage(void)
524 fprintf(stderr, "usage: %s [-qv] proto host port path\n",
525 getprogname());
526 exit(1);
529 int
530 main(int argc, char **argv)
532 struct pollfd pfd;
533 const char *host, *port, *path;
534 int https = 0;
535 int ch;
537 #ifndef PROFILE
538 if (pledge("stdio rpath inet dns unveil", NULL) == -1)
539 err(1, "pledge");
540 #endif
542 while ((ch = getopt(argc, argv, "qv")) != -1) {
543 switch (ch) {
544 case 'q':
545 verbose = -1;
546 break;
547 case 'v':
548 verbose++;
549 break;
550 default:
551 usage();
554 argc -= optind;
555 argv += optind;
557 if (argc != 4)
558 usage();
560 https = strcmp(argv[0], "https") == 0;
561 #ifndef PROFILE
562 if (https) {
563 if (unveil("/etc/ssl/cert.pem", "r") == -1)
564 err(1, "unveil /etc/ssl/cert.pem");
565 } else {
566 /* drop "rpath" */
567 if (pledge("stdio inet dns unveil", NULL) == -1)
568 err(1, "pledge");
570 #else
571 if (unveil("gmon.out", "rwc") != 0)
572 err(1, "unveil gmon.out");
573 #endif
574 if (unveil(NULL, NULL) == -1)
575 err(1, "unveil NULL");
577 host = argv[1];
578 port = argv[2];
579 path = argv[3];
581 if (get_refs(https, host, port, path) == -1)
582 errx(1, "failed to get refs");
584 pfd.fd = 0;
585 pfd.events = POLLIN;
586 if (poll(&pfd, 1, INFTIM) == -1)
587 err(1, "poll");
589 if ((ch = fgetc(stdin)) == EOF)
590 return 0;
592 ungetc(ch, stdin);
593 if (upload_request(https, host, port, path, stdin) == -1) {
594 fflush(tmp);
595 errx(1, "failed to upload request");
598 return 0;