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>
35 #include "got_error.h"
37 #include "got_version.h"
39 #include "got_lib_pkt.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)
56 bufio_getdelim_sync(struct bufio *bio, const char *nl, size_t *len)
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);
69 bufio_drain_sync(struct bufio *bio, void *d, size_t len)
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);
82 bufio_close_sync(struct bufio *bio)
88 if (r == -1 && errno == EAGAIN)
89 errx(1, "bufio_read: %s", bufio_io_err(bio));
90 } while (r == -1 && errno == EAGAIN);
94 hexstrtonum(const char *str, long long min, long long max, const char **errstr)
100 lval = strtoll(str, &cp, 16);
101 if (*str == '\0' || *cp != '\0') {
102 *errstr = "not a number";
105 if ((errno == ERANGE && (lval == LONG_MAX || lval == LONG_MIN)) ||
106 lval < min || lval > max) {
107 *errstr = "out of range";
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);
127 warnx("%s", gai_strerror(error));
131 for (res = res0; res; res = res->ai_next) {
132 fd = socket(res->ai_family, res->ai_socktype,
139 if (connect(fd, res->ai_addr, res->ai_addrlen) == 0)
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 = "";
166 if (strcmp(method, "POST") == 0)
167 te = "\r\nTransfer-Encoding: chunked\r\n";
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 : "");
177 r = asprintf(&req, "%s %s HTTP/1.1\r\n"
179 "Connection: close\r\n"
182 method, p, host, GOT_USERAGENT,
183 chdr ? chdr : "", ctype ? ctype : "", te);
189 fprintf(stderr, "%s: request: %s\n", getprogname(), req);
192 r = bufio_compose(bio, req, r);
194 err(1, "bufio_compose_fmt");
198 r = bufio_write(bio);
199 if (r == -1 && errno != EAGAIN)
200 errx(1, "bufio_read: %s", bufio_io_err(bio));
201 } while (bio->wbuf.len != 0);
207 http_parse_reply(struct bufio *bio, int *chunked, const char *expected_ctype)
214 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
216 warnx("%s: bufio_getdelim_sync()", __func__);
221 fprintf(stderr, "%s: response: %s\n", getprogname(), line);
223 if ((cp = strchr(line, ' ')) == NULL) {
224 warnx("malformed HTTP response");
229 if (strncmp(cp, "200 ", 4) != 0) {
230 warnx("malformed HTTP response");
233 buf_drain(&bio->rbuf, linelen);
236 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
238 warnx("%s: bufio_getdelim_sync()", __func__);
242 buf_drain(&bio->rbuf, linelen);
246 if (hasprfx(line, "content-type:")) {
247 cp = strchr(line, ':') + 1;
248 cp += strspn(cp, " \t");
249 cp[strcspn(cp, " \t")] = '\0';
250 if (strcmp(cp, expected_ctype) != 0) {
251 warnx("server not using the \"smart\" "
256 if (hasprfx(line, "transfer-encoding:")) {
257 cp = strchr(line, ':') + 1;
258 cp += strspn(cp, " \t");
259 cp[strcspn(cp, " \t")] = '\0';
260 if (strcmp(cp, "chunked") != 0) {
261 warnx("unknown transfer-encoding");
266 buf_drain(&bio->rbuf, linelen);
273 http_read(struct bufio *bio, int chunked, size_t *chunksz, char *buf, size_t bufsz)
278 ssize_t ret = 0, linelen;
281 return bufio_drain_sync(bio, buf, bufsz);
286 line = bufio_getdelim_sync(bio, "\r\n", &linelen);
288 buf_drain(&bio->rbuf, linelen);
292 buf_drain(&bio->rbuf, linelen);
293 goto again; /* was the CRLF after the chunk */
296 *chunksz = hexstrtonum(line, 0, INT_MAX, &errstr);
297 if (errstr != NULL) {
298 warnx("invalid HTTP chunk: size is %s (%s)",
305 buf_drain(&bio->rbuf, linelen);
308 buf_drain(&bio->rbuf, linelen);
311 r = bufio_drain_sync(bio, buf, MINIMUM(*chunksz, bufsz));
326 http_chunk(struct bufio *bio, const void *buf, size_t len)
330 if (bufio_compose_fmt(bio, "%zx\r\n", len) ||
331 bufio_compose(bio, buf, len) ||
332 bufio_compose(bio, "\r\n", 2))
336 r = bufio_write(bio);
337 if (r == -1 && errno != EAGAIN)
338 errx(1, "bufio_read: %s", bufio_io_err(bio));
339 } while (bio->wbuf.len != 0);
345 get_refs(int https, const char *host, const char *port, const char *path)
348 char buf[GOT_PKT_MAX];
349 const struct got_error *e;
357 if ((sock = dial(https, host, port)) == -1)
360 if (bufio_init(&bio)) {
364 bufio_set_fd(&bio, sock);
365 if (https && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1) {
366 warnx("bufio_starttls");
370 if (http_open(&bio, https, "GET", host, port, path, "info/refs",
371 "service=git-upload-pack", NULL) == -1)
374 /* Fetch the initial reference announcement from the server. */
375 if (http_parse_reply(&bio, &chunked, UPLOAD_PACK_ADV) == -1)
378 /* skip first pack; why git over http is like this? */
379 r = http_read(&bio, chunked, &chunksz, buf, 4);
383 e = got_pkt_readlen(&skip, buf, verbose);
389 /* TODO: validate it's # service=git-upload-pack\n */
391 r = http_read(&bio, chunked, &chunksz, buf,
392 MINIMUM(skip, sizeof(buf)));
399 r = http_read(&bio, chunked, &chunksz, buf, sizeof(buf));
406 fwrite(buf, 1, r, stdout);
412 bufio_close_sync(&bio);
418 upload_request(int https, const char *host, const char *port, const char *path,
422 char buf[GOT_PKT_MAX];
423 const struct got_error *e;
431 if ((sock = dial(https, host, port)) == -1)
434 if (bufio_init(&bio)) {
438 bufio_set_fd(&bio, sock);
439 if (https && bufio_starttls(&bio, host, 0, NULL, 0, NULL, 0) == -1) {
440 warnx("bufio_starttls");
444 /* TODO: can we push this upwards such that get_refs() is covered? */
445 if (pledge("stdio", NULL) == -1)
448 if (http_open(&bio, https, "POST", host, port, path, "git-upload-pack",
449 NULL, UPLOAD_PACK_REQ) == -1)
453 * Read have/want lines generated by got-fetch-pack and forward
454 * them to the server in the POST request body.
457 r = fread(buf, 1, 4, in);
461 e = got_pkt_readlen(&t, buf, verbose);
468 const char *flushpkt = "0000";
469 if (http_chunk(&bio, flushpkt, strlen(flushpkt)))
471 continue; /* got-fetch-pack will send "done" */
475 warnx("pktline len is too small");
479 r = fread(buf + 4, 1, t - 4, in);
483 if (http_chunk(&bio, buf, t))
487 * Once got-fetch-pack is done the server will
488 * send pack file data.
490 if (t == 9 && strncmp(buf + 4, "done\n", 5) == 0) {
491 if (http_chunk(&bio, NULL, 0))
497 if (http_parse_reply(&bio, &chunked, UPLOAD_PACK_RES) == -1)
500 /* Fetch pack file data from server. */
502 r = http_read(&bio, chunked, &chunksz, buf, sizeof(buf));
509 fwrite(buf, 1, r, stdout);
514 bufio_close_sync(&bio);
522 fprintf(stderr, "usage: %s [-qv] proto host port path\n",
528 main(int argc, char **argv)
531 const char *host, *port;
537 if (pledge("stdio rpath inet dns unveil", NULL) == -1)
541 while ((ch = getopt(argc, argv, "qv")) != -1) {
559 https = strcmp(argv[0], "https") == 0;
562 if (unveil("/etc/ssl/cert.pem", "r") == -1)
563 err(1, "unveil /etc/ssl/cert.pem");
566 if (pledge("stdio inet dns unveil", NULL) == -1)
570 if (unveil("gmon.out", "rwc") != 0)
571 err(1, "unveil gmon.out");
573 if (unveil(NULL, NULL) == -1)
574 err(1, "unveil NULL");
579 got_path_strip_trailing_slashes(path);
581 if (get_refs(https, host, port, path) == -1)
582 errx(1, "failed to get refs");
586 if (poll(&pfd, 1, INFTIM) == -1)
589 if ((ch = fgetc(stdin)) == EOF)
593 if (upload_request(https, host, port, path, stdin) == -1) {
595 errx(1, "failed to upload request");