"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Omar Polo <op@omarpolo.com>
Subject:
WIP: read-only https support
To:
gameoftrees@openbsd.org
Date:
Sun, 20 Nov 2022 10:45:14 +0100

Download raw body.

Thread
just scratching an itch; there are plenty of web forges online and I
don't have an account for every one of them (not that I want to).  I
can use git to clone, but then i'd miss all the niceties :)

Diff belows adds an initial read-only HTTP/S support for got fetch and
clone.  The code is incomplete, wip, etc... use it at your own risk.
Sharing just in case somebody wants to play along.

This is done with a new libexec helper "got-http" (an alternative name
could be "got-dial-http"?)  To minimize the changes needed to the dial
and fetch_pack API I decided to write an helper that behaves like
ssh(1) as far as got is concerned.  Under the hood, it transforms what
got asks into HTTP requests.  Only the "smart" HTTP protocol is
supported, the "dumb" one not.  (as of now at least)

The "smart" HTTP protocol behaves almost as git over ssh, but needs
two HTTP requests:

 - a first one to do the "discovery" (see if the remote server is
   "smart") and fetch the refs
 - a POST where we send our have/want line and fetch the packfile

The dumb one is just a bare git repo served via a web server (could be
httpd(8)) and needs us to fetch all the objects manually and do the
resolving by ourselves.  To be fair I'm not thrilled at the idea of
implementing it.

got-http is pledged "stdio inet dns" and is not unveiled by default
unlike the other libexec helpers.  It also can't be sandboxed with
capsicum(4) on FreeBSD and I don't want to go thru the pain of trying
to sandbox it with landlock on linux (needs to access certs.pem and
probably more stuff there?)

At the moment it "works."  I managed to clone repos from github
(including ports.git) and from sr.ht.  Incremental fetches also seems
to work, in part at least.  There's still some bits of how the server
replies that I'm not following.  For example, here's an excerpt of a
partial fetch:

00000000  30 30 30 38 4e 41 4b 0a  30 30 32 39 02 45 6e 75  |0008NAK.0029.Enu|
00000010  6d 65 72 61 74 69 6e 67  20 6f 62 6a 65 63 74 73  |merating objects|
00000020  3a 20 31 37 37 33 39 32  34 2c 20 64 6f 6e 65 2e  |: 1773924, done.|
00000030  0a 30 30 32 36 02 43 6f  75 6e 74 69 6e 67 20 6f  |.0026.Counting o|
...
000021a0  30 25 20 28 37 39 34 30  2f 37 39 34 30 29 2c 20  |0% (7940/7940), |
000021b0  64 6f 6e 65 2e 0a 30 30  31 30 01 50 41 43 4b 00  |done..0010.PACK.|
000021c0  00 00 02 00 1b 11 32 30  30 35 01 64 93 16 78 9c  |......2005.d..x.|

We get a NAK and then side-band info, which seems to confuse
got-fetch-pack that excepts at least one ACK.  (see the XXX below.)

In some case partial fetches seems to degrade into a full fetch, still
investigating.


diff /home/op/w/gotd
commit - 4cad5be9f88baeb0583b4b63a546f5815929a270
path + /home/op/w/gotd
blob - ad0fec63ec6030f40937e7bf6836769359bb55ba
file + got/got.c
--- got/got.c
+++ got/got.c
@@ -1649,16 +1649,16 @@ cmd_clone(int argc, char *argv[])
 			err(1, "pledge");
 #endif
 	} else if (strcmp(proto, "git+ssh") == 0 ||
-	    strcmp(proto, "ssh") == 0) {
+	    strcmp(proto, "ssh") == 0 ||
+	    strcmp(proto, "git+http") == 0 ||
+	    strcmp(proto, "http") == 0 ||
+	    strcmp(proto, "git+https") == 0 ||
+	    strcmp(proto, "https") == 0) {
 #ifndef PROFILE
 		if (pledge("stdio rpath wpath cpath fattr flock proc exec "
 		    "sendfd unveil", NULL) == -1)
 			err(1, "pledge");
 #endif
-	} else if (strcmp(proto, "http") == 0 ||
-	    strcmp(proto, "git+http") == 0) {
-		error = got_error_path(proto, GOT_ERR_NOT_IMPL);
-		goto done;
 	} else {
 		error = got_error_path(proto, GOT_ERR_BAD_PROTO);
 		goto done;
@@ -2514,16 +2514,16 @@ cmd_fetch(int argc, char *argv[])
 			err(1, "pledge");
 #endif
 	} else if (strcmp(proto, "git+ssh") == 0 ||
-	    strcmp(proto, "ssh") == 0) {
+	    strcmp(proto, "ssh") == 0 ||
+	    strcmp(proto, "git+http") == 0 ||
+	    strcmp(proto, "http") == 0 ||
+	    strcmp(proto, "git+https") == 0 ||
+	    strcmp(proto, "https") == 0) {
 #ifndef PROFILE
 		if (pledge("stdio rpath wpath cpath fattr flock proc exec "
 		    "sendfd unveil", NULL) == -1)
 			err(1, "pledge");
 #endif
-	} else if (strcmp(proto, "http") == 0 ||
-	    strcmp(proto, "git+http") == 0) {
-		error = got_error_path(proto, GOT_ERR_NOT_IMPL);
-		goto done;
 	} else {
 		error = got_error_path(proto, GOT_ERR_BAD_PROTO);
 		goto done;
blob - 3325c8994f55721d8588155cdc72b95d11fd2248
file + lib/dial.c
--- lib/dial.c
+++ lib/dial.c
@@ -18,19 +18,28 @@
 #include <sys/queue.h>
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <sys/uio.h>
 #include <netdb.h>
 
 #include <assert.h>
 #include <err.h>
+#include <limits.h>
+#include <sha1.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <imsg.h>
 
 #include "got_error.h"
 #include "got_path.h"
+#include "got_object.h"
 
 #include "got_lib_dial.h"
+#include "got_lib_delta.h"
+#include "got_lib_object.h"
+#include "got_lib_privsep.h"
 #include "got_dial.h"
 
 #ifndef nitems
@@ -63,6 +72,13 @@ got_dial_apply_unveil(const char *proto)
 		}
 	}
 
+	if (strstr(proto, "http") != NULL) {
+		if (unveil(GOT_PATH_PROG_HTTP, "x") != 0) {
+			return got_error_from_errno2("unveil",
+			    GOT_PATH_PROG_HTTP);
+		}
+	}
+
 	return NULL;
 }
 
@@ -321,3 +337,59 @@ done:
 		*newfd = fd;
 	return err;
 }
+
+const struct got_error *
+got_dial_http(pid_t *newpid, int *newfd, const char *host,
+    const char *port, const char *path, int verbosity, int tls)
+{
+	const struct got_error *error = NULL;
+	int pid, pfd[2];
+	const char *argv[8];
+	int i = 0;
+
+	*newpid = -1;
+	*newfd = -1;
+
+	if (!port)
+		port = tls ? "443" : "80";
+
+	argv[i++] = GOT_PATH_PROG_HTTP;
+	if (verbosity == -1)
+		argv[i++] = "-q";
+	else if (verbosity > 0)
+		argv[i++] = "-v";
+	argv[i++] = "--";
+	argv[i++] = tls ? "https" : "http";
+	argv[i++] = host;
+	argv[i++] = port;
+	argv[i++] = path;
+	argv[i++] = NULL;
+	assert(i <= nitems(argv));
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pfd) == -1)
+		return got_error_from_errno("socketpair");
+
+	pid = fork();
+	if (pid == -1) {
+		error = got_error_from_errno("fork");
+		close(pfd[0]);
+		close(pfd[1]);
+		return error;
+	} else if (pid == 0) {
+		if (close(pfd[1]) == -1)
+			err(1, "close");
+		if (dup2(pfd[0], 0) == -1)
+			err(1, "dup2");
+		if (dup2(pfd[0], 1) == -1)
+			err(1, "dup2");
+		if (execv(GOT_PATH_PROG_HTTP, (char *const *)argv) == -1)
+			err(1, "execv");
+		abort(); /* not reached */
+	} else {
+		if (close(pfd[0]) == -1)
+			return got_error_from_errno("close");
+		*newpid = pid;
+		*newfd = pfd[1];
+		return NULL;
+	}
+}
blob - 0dd641076040553ff06ef8320ae4686dabdd6786
file + lib/fetch.c
--- lib/fetch.c
+++ lib/fetch.c
@@ -91,8 +91,12 @@ got_fetch_connect(pid_t *fetchpid, int *fetchfd, const
 	else if (strcmp(proto, "git") == 0)
 		err = got_dial_git(fetchfd, host, port, server_path,
 		    GOT_DIAL_DIRECTION_FETCH);
-	else if (strcmp(proto, "http") == 0 || strcmp(proto, "git+http") == 0)
-		err = got_error_path(proto, GOT_ERR_NOT_IMPL);
+	else if (strcmp(proto, "http") == 0 ||
+	    strcmp(proto, "git+http") == 0 ||
+	    strcmp(proto, "https") == 0 ||
+	    strcmp(proto, "git+https") == 0)
+		err = got_dial_http(fetchpid, fetchfd, host, port,
+		    server_path, verbosity, strstr(proto, "https") != NULL);
 	else
 		err = got_error_path(proto, GOT_ERR_BAD_PROTO);
 	return err;
blob - cbaf4ea224445b13ae795d7ec9ddb72fda40ab52
file + lib/got_lib_dial.h
--- lib/got_lib_dial.h
+++ lib/got_lib_dial.h
@@ -24,3 +24,6 @@ const struct got_error *got_dial_ssh(pid_t *newpid, in
 const struct got_error *got_dial_ssh(pid_t *newpid, int *newfd,
     const char *host, const char *port, const char *path,
     const char *direction, int verbosity);
+
+const struct got_error *got_dial_http(pid_t *newpid, int *newfd,
+    const char *host, const char *port, const char *path, int, int);
blob - 37d537ab48623fa50dd909cf2725e58151958e35
file + lib/got_lib_privsep.h
--- lib/got_lib_privsep.h
+++ lib/got_lib_privsep.h
@@ -48,6 +48,7 @@
 #define GOT_PROG_FETCH_PACK	got-fetch-pack
 #define GOT_PROG_INDEX_PACK	got-index-pack
 #define GOT_PROG_SEND_PACK	got-send-pack
+#define GOT_PROG_HTTP		got-http
 
 #define GOT_STRINGIFY(x) #x
 #define GOT_STRINGVAL(x) GOT_STRINGIFY(x)
@@ -77,6 +78,8 @@
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_SEND_PACK)
 #define GOT_PATH_PROG_INDEX_PACK \
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_INDEX_PACK)
+#define GOT_PATH_PROG_HTTP \
+	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_HTTP)
 
 enum got_imsg_type {
 	/* An error occured while processing a request. */
blob - 174d412f27d59a5180400ca4bbd33f6a4b8e5564
file + lib/privsep.c
--- lib/privsep.c
+++ lib/privsep.c
@@ -3483,6 +3483,7 @@ got_privsep_unveil_exec_helpers(void)
 	    GOT_PATH_PROG_FETCH_PACK,
 	    GOT_PATH_PROG_INDEX_PACK,
 	    GOT_PATH_PROG_SEND_PACK,
+	    /* GOT_PATH_PROG_HTTP explicitly excluded */
 	};
 	size_t i;
 
blob - cfd4876a2dfa135816bb51fb862396c0cd6a4331
file + libexec/Makefile
--- libexec/Makefile
+++ libexec/Makefile
@@ -1,6 +1,6 @@
 SUBDIR = got-read-blob got-read-commit got-read-object got-read-tree \
 	got-read-tag got-fetch-pack got-index-pack got-read-pack \
 	got-read-gitconfig got-read-gotconfig got-send-pack \
-	got-read-patch
+	got-read-patch got-http
 
 .include <bsd.subdir.mk>
blob - 07e2042c17a6d66588622b9d6a7fb1e9c158cfbd
file + libexec/got-fetch-pack/got-fetch-pack.c
--- libexec/got-fetch-pack/got-fetch-pack.c
+++ libexec/got-fetch-pack/got-fetch-pack.c
@@ -541,6 +541,10 @@ fetch_pack(int fd, int packfd, uint8_t *pack_sha1,
 			/* Server has not located our objects yet. */
 			continue;
 		}
+		if (n > 1 && buf[0] == 0x2) {
+			/* XXX: sideband? */
+			break;
+		}
 		if (n < 4 + SHA1_DIGEST_STRING_LENGTH ||
 		    strncmp(buf, "ACK ", 4) != 0) {
 			err = got_error_msg(GOT_ERR_BAD_PACKET,
blob - /dev/null
file + libexec/got-http/Makefile (mode 644)
--- /dev/null
+++ libexec/got-http/Makefile
@@ -0,0 +1,18 @@
+.PATH:${.CURDIR}/../../lib
+
+.include "../../got-version.mk"
+
+PROG=		got-http
+SRCS=		got-http.c error.c inflate.c sha1.c pollfd.c
+
+CPPFLAGS= -I${.CURDIR}/../../include -I${.CURDIR}/../../lib
+
+.if defined(PROFILE)
+LDADD=	-lutil_p -lz_p -ltls_p
+.else
+LDADD=	-lutil -lz -ltls
+.endif
+
+DPADD=	${LIBZ} ${LIBUTIL} ${LIBTLS}
+
+.include <bsd.prog.mk>
blob - /dev/null
file + libexec/got-http/got-http.c (mode 644)
--- /dev/null
+++ libexec/got-http/got-http.c
@@ -0,0 +1,608 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <netdb.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <tls.h>
+#include <unistd.h>
+
+#include "got_version.h"
+
+#define UPLOAD_PACK_ADV "application/x-git-upload-pack-advertisement"
+#define UPLOAD_PACK_REQ "application/x-git-upload-pack-request"
+#define UPLOAD_PACK_RES "application/x-git-upload-pack-result"
+
+#define HTTP_BUFSIZ	4096
+#define	GOT_USERAGENT	"got/" GOT_VERSION_STR
+#define MINIMUM(a, b)	((a) < (b) ? (a) : (b))
+#define hasprfx(str, p)	(strncasecmp(str, p, strlen(p)) == 0)
+
+#define DEBUG_HTTP 1
+
+FILE *tmp;
+
+static int	verbose;
+
+static long long
+hexstrtonum(const char *str, long long min, long long max, const char **errstr)
+{
+	long long	 lval;
+	char		*cp;
+
+	errno = 0;
+	lval = strtoll(str, &cp, 16);
+	if (*str == '\0' || *cp != '\0') {
+		*errstr = "not a number";
+		return 0;
+	}
+	if ((errno == ERANGE && (lval == LONG_MAX || lval == LONG_MIN)) ||
+	    lval < min || lval > max) {
+		*errstr = "out of range";
+		return 0;
+	}
+
+	*errstr = NULL;
+	return lval;
+}
+
+static int
+stdio_tls_write(void *arg, const char *buf, int len)
+{
+	struct tls	*ctx = arg;
+	ssize_t		 ret;
+
+	do {
+		ret = tls_write(ctx, buf, len);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+
+	if (ret == -1)
+		warn("tls_write: %s", tls_error(ctx));
+
+	return ret;
+}
+
+static int
+stdio_tls_read(void *arg, char *buf, int len)
+{
+	struct tls	*ctx = arg;
+	ssize_t		 ret;
+
+	do {
+		ret = tls_read(ctx, buf, len);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+
+	if (ret == -1)
+		warn("tls_read: %s", tls_error(ctx));
+
+	return ret;
+}
+
+static int
+stdio_tls_close(void *arg)
+{
+	struct tls	*ctx = arg;
+	int		 ret;
+
+	do {
+		ret = tls_close(ctx);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+
+	return ret;
+}
+
+static FILE *
+dial(int https, const char *host, const char *port)
+{
+	FILE			*fp;
+	struct tls		*ctx;
+	struct tls_config	*conf;
+	struct addrinfo		 hints, *res, *res0;
+	int			 r, error, saved_errno, fd = -1;
+	const char		*cause = NULL;
+
+	if (https) {
+		if ((conf = tls_config_new()) == NULL)
+			errx(1, "failed to create TLS configuration");
+		if ((ctx = tls_client()) == NULL)
+			errx(1, "failed to create TLS client");
+		if (tls_configure(ctx, conf) == -1)
+			errx(1, "TLS configuration failure: %s",
+			    tls_error(ctx));
+		tls_config_free(conf);
+
+		if (tls_connect(ctx, host, port) == -1) {
+			warnx("connect to %s:%s: %s", host, port,
+			    tls_error(ctx));
+			tls_close(ctx);
+			return NULL;
+		}
+		do {
+			r = tls_handshake(ctx);
+		} while (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT);
+		fp = funopen(ctx, stdio_tls_read, stdio_tls_write, NULL,
+		    stdio_tls_close);
+		if (fp == NULL) {
+			warn("funopen");
+			tls_free(ctx);
+		}
+		return fp;
+	}
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	error = getaddrinfo(host, port, &hints, &res0);
+	if (error) {
+		warnx("%s", gai_strerror(error));
+		return NULL;
+	}
+
+	for (res = res0; res; res = res->ai_next) {
+		fd = socket(res->ai_family, res->ai_socktype,
+		    res->ai_protocol);
+		if (fd == -1) {
+			cause = "socket";
+			continue;
+		}
+
+		if (connect(fd, res->ai_addr, res->ai_addrlen) == 0)
+			break;
+
+		cause = "connect";
+		saved_errno = errno;
+		close(fd);
+		fd = -1;
+		errno = saved_errno;
+	}
+	freeaddrinfo(res0);
+
+	if (fd == -1) {
+		warn("%s", cause);
+		return NULL;
+	}
+
+	if ((fp = fdopen(fd, "r+")) == NULL) {
+		warn("fdopen");
+		close(fd);
+	}
+	return fp;
+}
+
+static FILE *
+http_open(int https, const char *method, const char *host, const char *port,
+    const char *path, const char *path_sufx, const char *query,
+    const char *ctype)
+{
+	FILE		*fp;
+	const char	*chdr = NULL, *te = "";
+	char		*p, *req;
+	int		 r;
+
+	if ((fp = dial(https, host, port)) == NULL)
+		return NULL;
+
+	if (path_sufx != NULL && *path && path[strlen(path) - 1] == '/')
+		path_sufx++; /* skip the slash */
+
+	if (strcmp(method, "POST") == 0)
+		te = "\r\nTransfer-Encoding: chunked\r\n";
+
+	if (ctype)
+		chdr = "Content-Type: ";
+
+	r = asprintf(&p, "%s/%s%s%s", path, path_sufx,
+	    query ? "?" : "", query ? query : "");
+	if (r == -1)
+		err(1, "asprintf");
+
+	r = asprintf(&req, "%s %s HTTP/1.1\r\n"
+	    "Host: %s\r\n"
+	    "Connection: close\r\n"
+	    "User-agent: %s\r\n"
+	    "%s%s%s\r\n",
+	    method, p, host, GOT_USERAGENT,
+	    chdr ? chdr : "", ctype ? ctype : "", te);
+	free(p);
+	if (r == -1)
+		err(1, "asprintf");
+
+	if (verbose > 0)
+		fprintf(stderr, "%s: %s", getprogname(), req);
+
+	if (fwrite(req, 1, r, fp) != r) {
+		free(req);
+		fclose(fp);
+		return NULL;
+	}
+	free(req);
+
+	return fp;
+}
+
+static int
+http_parse_reply(FILE *fp, int *chunked, const char *expected_ctype)
+{
+	char		*cp, *line = NULL;
+	size_t		 linesize = 0;
+	ssize_t		 linelen;
+
+	*chunked = 0;
+
+	if ((linelen = getline(&line, &linesize, fp)) == -1) {
+		warn("%s: getline", __func__);
+		return -1;
+	}
+
+	if ((cp = strchr(line, '\r')) == NULL) {
+		warnx("malformed HTTP response");
+		goto err;
+	}
+	*cp = '\0';
+
+	if ((cp = strchr(line, ' ')) == NULL) {
+		warnx("malformed HTTP response");
+		goto err;
+	}
+	cp++;
+
+	if (strncmp(cp, "200 ", 4) != 0) {
+		warnx("malformed HTTP response");
+		goto err;
+	}
+
+	while ((linelen = getline(&line, &linesize, fp)) != -1) {
+		if (line[linelen-1] == '\n')
+			line[--linelen] = '\0';
+		if (linelen > 0 && line[linelen-1] == '\r')
+			line[--linelen] = '\0';
+
+		if (*line == '\0')
+			break;
+
+		if (hasprfx(line, "content-type:")) {
+			cp = strchr(line, ':') + 1;
+			cp += strspn(cp, " \t");
+			cp[strcspn(cp, " \t")] = '\0';
+			if (strcmp(cp, expected_ctype) != 0) {
+				warnx("server not using the \"smart\" "
+				    "HTTP protocol.");
+				goto err;
+			}
+		}
+
+		if (hasprfx(line, "transfer-encoding:")) {
+			cp = strchr(line, ':') + 1;
+			cp += strspn(cp, " \t");
+			cp[strcspn(cp, " \t")] = '\0';
+			if (strcmp(cp, "chunked") != 0) {
+				warnx("unknown transfer-encoding");
+				goto err;
+			}
+			*chunked = 1;
+		}
+	}
+
+	free(line);
+	return 0;
+
+err:
+	free(line);
+	return -1;
+}
+
+static ssize_t
+http_read(FILE *fp, int chunked, size_t *chunksz, void *buf, size_t bufsz)
+{
+	const char	*errstr;
+	char		*cp, *line = NULL;
+	size_t		 r, linesize = 0;
+	ssize_t		 ret = 0, linelen;
+
+	if (!chunked) {
+		r = fread(buf, 1, bufsz, fp);
+		if (r == 0 && ferror(fp))
+			return -1;
+#if DEBUG_HTTP
+		fwrite(buf, 1, r, stderr);
+#endif
+		return r;
+	}
+
+	while (bufsz > 0) {
+		if (*chunksz == 0) {
+		again:
+			if ((linelen = getline(&line, &linesize, fp)) == -1) {
+				if (ferror(fp)) {
+					warn("%s: getline", __func__);
+					ret = -1;
+				}
+				break;
+			}
+
+			if ((cp = strchr(line, '\r')) == NULL) {
+				warnx("invalid HTTP chunk: missing CR");
+				ret = -1;
+				break;
+			}
+			*cp = '\0';
+
+			if (*line == '\0')
+				goto again; /* was the CRLF after the chunk */
+
+			*chunksz = hexstrtonum(line, 0, INT_MAX, &errstr);
+			if (errstr != NULL) {
+				warnx("invalid HTTP chunk: size is %s (%s)",
+				    errstr, line);
+				ret = -1;
+				break;
+			}
+
+			if (*chunksz == 0)
+				break;
+		}
+
+		r = fread(buf, 1, MINIMUM(*chunksz, bufsz), fp);
+		if (r == 0) {
+			if (ferror(fp))
+				ret = -1;
+			break;
+		}
+
+#if DEBUG_HTTP
+		if (tmp)
+			fwrite(buf, 1, r, tmp);
+		/* fwrite(buf, 1, r, stderr); */
+#endif
+		ret += r;
+		buf += r;
+		bufsz -= r;
+		*chunksz -= r;
+	}
+
+	free(line);
+	return ret;
+}
+
+static void
+http_chunk(FILE *fp, const void *buf, size_t len)
+{
+	/* fprintf(stderr, "> %.*s", (int)len, (char *)buf); */
+
+	fprintf(fp, "%zx\r\n", len);
+	if (fwrite(buf, 1, len, fp) != len ||
+	    fwrite("\r\n", 1, 2, fp) != 2)
+		err(1, "%s fwrite", __func__);
+}
+
+static int
+get_refs(int https, const char *host, const char *port, const char *path)
+{
+	char		 buf[HTTP_BUFSIZ];
+	const char	*errstr, *sufx = "/info/refs";
+	FILE		*fp;
+	size_t		 skip, chunksz = 0;
+	ssize_t		 r;
+	int		 chunked;
+
+	fp = http_open(https, "GET", host, port, path, sufx,
+	    "service=git-upload-pack", NULL);
+	if (fp == NULL)
+		return -1;
+
+	if (http_parse_reply(fp, &chunked, UPLOAD_PACK_ADV) == -1) {
+		fclose(fp);
+		return -1;
+	}
+
+	/* skip first pack; why git over http is like this? */
+	r = http_read(fp, chunked, &chunksz, buf, 4);
+	if (r <= 0) {
+		fclose(fp);
+		return -1;
+	}
+	buf[4] = '\0';
+	skip = hexstrtonum(buf, 0, INT_MAX, &errstr);
+	if (errstr != NULL) {
+		warnx("pktlen is %s", errstr);
+		fclose(fp);
+		return -1;
+	}
+
+	/* TODO: validate it's # service=git-upload-pack\n */
+	while (skip > 0) {
+		r = http_read(fp, chunked, &chunksz, buf,
+		    MINIMUM(skip, sizeof(buf)));
+		if (r <= 0) {
+			fclose(fp);
+			return -1;
+		}
+
+		skip -= r;
+	}
+
+	for (;;) {
+		r = http_read(fp, chunked, &chunksz, buf, sizeof(buf));
+		if (r == -1) {
+			fclose(fp);
+			return -1;
+		}
+
+		if (r == 0)
+			break;
+
+		fwrite(buf, 1, r, stdout);
+	}
+
+	fflush(stdout);
+	fclose(fp);
+	return 0;
+}
+
+static int
+upload_request(int https, const char *host, const char *port, const char *path,
+    FILE *in)
+{
+	const char	*errstr;
+	char		 buf[HTTP_BUFSIZ];
+	FILE		*fp;
+	ssize_t		 r;
+	size_t		 chunksz = 0;
+	long long	 t;
+	int		 chunked;
+
+	fp = http_open(https, "POST", host, port, path, "/git-upload-pack",
+	    NULL, UPLOAD_PACK_REQ);
+	if (fp == NULL)
+		return -1;
+
+	for (;;) {
+		r = fread(buf, 1, 4, in);
+		if (r != 4)
+			goto err;
+
+		buf[4] = '\0';
+		t = hexstrtonum(buf, 0, sizeof(buf), &errstr);
+		if (errstr != NULL) {
+			warnx("pktline len is %s", errstr);
+			goto err;
+		}
+
+		/* no idea why 0000 is not enough. */
+		if (t == 0) {
+			const char *x = "00000009done\n";
+			http_chunk(fp, x, strlen(x));
+			http_chunk(fp, NULL, 0);
+			break;
+		}
+
+		if (t < 6) {
+			warnx("pktline len is too small");
+			goto err;
+		}
+
+		r = fread(buf + 4, 1, t - 4, in);
+		if (r != t - 4)
+			goto err;
+
+		http_chunk(fp, buf, t);
+	}
+
+	if (http_parse_reply(fp, &chunked, UPLOAD_PACK_RES) == -1)
+		goto err;
+
+	for (;;) {
+		r = http_read(fp, chunked, &chunksz, buf, sizeof(buf));
+		if (r == -1) {
+			fclose(fp);
+			return -1;
+		}
+
+		if (r == 0)
+			break;
+
+		fwrite(buf, 1, r, stdout);
+	}
+
+	fclose(fp);
+	return 0;
+
+err:
+	fclose(fp);
+	return -1;
+}
+
+static __dead void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-qv] proto host port path\n",
+	     getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	struct pollfd	 pfd;
+	const char	*host, *port, *path;
+	int		 https = 0;
+	int		 ch;
+#if 0
+	static int attached;
+	while (!attached)
+		sleep(1);
+#endif
+
+#if !DEBUG_HTTP || defined(PROFILE)
+	if (pledge("stdio inet dns", NULL) == -1)
+		err(1, "pledge");
+#endif
+
+	while ((ch = getopt(argc, argv, "qv")) != -1) {
+		switch (ch) {
+		case 'q':
+			verbose = -1;
+			break;
+		case 'v':
+			verbose++;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 4)
+		usage();
+
+	https = strcmp(argv[0], "https") == 0;
+
+	host = argv[1];
+	port = argv[2];
+	path = argv[3];
+
+	if (get_refs(https, host, port, path) == -1)
+		errx(1, "failed to get refs");
+
+#if DEBUG_HTTP
+	tmp = fopen("/tmp/pck", "w");
+#endif
+
+	pfd.fd = 0;
+	pfd.events = POLLIN;
+	if (poll(&pfd, 1, INFTIM) == -1)
+		err(1, "poll");
+
+	if ((ch = fgetc(stdin)) == EOF)
+		return 0;
+
+	ungetc(ch, stdin);
+	if (upload_request(https, host, port, path, stdin) == -1) {
+		fflush(tmp);
+		errx(1, "failed to upload request");
+	}
+
+	return 0;
+}
blob - 45aaadb8bcd9513fe5075ceaa0fc416d03874885
file + libexec/got-read-gotconfig/got-read-gotconfig.c
--- libexec/got-read-gotconfig/got-read-gotconfig.c
+++ libexec/got-read-gotconfig/got-read-gotconfig.c
@@ -383,7 +383,11 @@ validate_protocol(const char *protocol, const char *re
 
 	if (strcmp(protocol, "ssh") != 0 &&
 	    strcmp(protocol, "git+ssh") != 0 &&
-	    strcmp(protocol, "git") != 0) {
+	    strcmp(protocol, "git") != 0 &&
+	    strcmp(protocol, "git+http") != 0 &&
+	    strcmp(protocol, "http") != 0 &&
+	    strcmp(protocol, "https") != 0 &&
+	    strcmp(protocol, "git+https") != 0) {
 		snprintf(msg, sizeof(msg),"unknown protocol \"%s\" "
 		    "for remote repository \"%s\"", protocol, repo_name);
 		return got_error_msg(GOT_ERR_PARSE_CONFIG, msg);