remote-curl: implement stateless-connect command

Teach remote-curl the 'stateless-connect' command which is used to establish a stateless connection with servers which support protocol version 2. This allows remote-curl to act as a proxy, allowing the git client to communicate natively with a remote end, simply using remote-curl as a pass through to convert requests to http. Signed-off-by: Brandon Williams <bmwill@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Brandon Williams committed Mar 15, 2018 at 10:31 UTC 0f1dc53f45d307c8342bf547d79695b614cf9c26
2 files changed +251 -1
remote-curl.c
+206 -1
@@ -188,7 +188,12 @@ static struct ref *parse_git_refs(struct discovery *heads, int for_push)
188 heads->version = discover_version(&reader);
189 switch (heads->version) {
190 case protocol_v2:
191 - die("support for protocol v2 not implemented yet");
191 + /*
192 + * Do nothing. This isn't a list of refs but rather a
193 + * capability advertisement. Client would have run
194 + * 'stateless-connect' so we'll dump this capability listing
195 + * and let them request the refs themselves.
196 + */
197 break;
198 case protocol_v1:
199 case protocol_v0:
@@ -1085,6 +1090,202 @@ static void parse_push(struct strbuf *buf)
1090 free(specs);
1091 }
1092
1093 +/*
1094 + * Used to represent the state of a connection to an HTTP server when
1095 + * communicating using git's wire-protocol version 2.
1096 + */
1097 +struct proxy_state {
1098 + char *service_name;
1099 + char *service_url;
1100 + struct curl_slist *headers;
1101 + struct strbuf request_buffer;
1102 + int in;
1103 + int out;
1104 + struct packet_reader reader;
1105 + size_t pos;
1106 + int seen_flush;
1107 +};
1108 +
1109 +static void proxy_state_init(struct proxy_state *p, const char *service_name,
1110 + enum protocol_version version)
1111 +{
1112 + struct strbuf buf = STRBUF_INIT;
1113 +
1114 + memset(p, 0, sizeof(*p));
1115 + p->service_name = xstrdup(service_name);
1116 +
1117 + p->in = 0;
1118 + p->out = 1;
1119 + strbuf_init(&p->request_buffer, 0);
1120 +
1121 + strbuf_addf(&buf, "%s%s", url.buf, p->service_name);
1122 + p->service_url = strbuf_detach(&buf, NULL);
1123 +
1124 + p->headers = http_copy_default_headers();
1125 +
1126 + strbuf_addf(&buf, "Content-Type: application/x-%s-request", p->service_name);
1127 + p->headers = curl_slist_append(p->headers, buf.buf);
1128 + strbuf_reset(&buf);
1129 +
1130 + strbuf_addf(&buf, "Accept: application/x-%s-result", p->service_name);
1131 + p->headers = curl_slist_append(p->headers, buf.buf);
1132 + strbuf_reset(&buf);
1133 +
1134 + p->headers = curl_slist_append(p->headers, "Transfer-Encoding: chunked");
1135 +
1136 + /* Add the Git-Protocol header */
1137 + if (get_protocol_http_header(version, &buf))
1138 + p->headers = curl_slist_append(p->headers, buf.buf);
1139 +
1140 + packet_reader_init(&p->reader, p->in, NULL, 0,
1141 + PACKET_READ_GENTLE_ON_EOF);
1142 +
1143 + strbuf_release(&buf);
1144 +}
1145 +
1146 +static void proxy_state_clear(struct proxy_state *p)
1147 +{
1148 + free(p->service_name);
1149 + free(p->service_url);
1150 + curl_slist_free_all(p->headers);
1151 + strbuf_release(&p->request_buffer);
1152 +}
1153 +
1154 +/*
1155 + * CURLOPT_READFUNCTION callback function.
1156 + * Attempts to copy over a single packet-line at a time into the
1157 + * curl provided buffer.
1158 + */
1159 +static size_t proxy_in(char *buffer, size_t eltsize,
1160 + size_t nmemb, void *userdata)
1161 +{
1162 + size_t max;
1163 + struct proxy_state *p = userdata;
1164 + size_t avail = p->request_buffer.len - p->pos;
1165 +
1166 +
1167 + if (eltsize != 1)
1168 + BUG("curl read callback called with size = %"PRIuMAX" != 1",
1169 + (uintmax_t)eltsize);
1170 + max = nmemb;
1171 +
1172 + if (!avail) {
1173 + if (p->seen_flush) {
1174 + p->seen_flush = 0;
1175 + return 0;
1176 + }
1177 +
1178 + strbuf_reset(&p->request_buffer);
1179 + switch (packet_reader_read(&p->reader)) {
1180 + case PACKET_READ_EOF:
1181 + die("unexpected EOF when reading from parent process");
1182 + case PACKET_READ_NORMAL:
1183 + packet_buf_write_len(&p->request_buffer, p->reader.line,
1184 + p->reader.pktlen);
1185 + break;
1186 + case PACKET_READ_DELIM:
1187 + packet_buf_delim(&p->request_buffer);
1188 + break;
1189 + case PACKET_READ_FLUSH:
1190 + packet_buf_flush(&p->request_buffer);
1191 + p->seen_flush = 1;
1192 + break;
1193 + }
1194 + p->pos = 0;
1195 + avail = p->request_buffer.len;
1196 + }
1197 +
1198 + if (max < avail)
1199 + avail = max;
1200 + memcpy(buffer, p->request_buffer.buf + p->pos, avail);
1201 + p->pos += avail;
1202 + return avail;
1203 +}
1204 +
1205 +static size_t proxy_out(char *buffer, size_t eltsize,
1206 + size_t nmemb, void *userdata)
1207 +{
1208 + size_t size;
1209 + struct proxy_state *p = userdata;
1210 +
1211 + if (eltsize != 1)
1212 + BUG("curl read callback called with size = %"PRIuMAX" != 1",
1213 + (uintmax_t)eltsize);
1214 + size = nmemb;
1215 +
1216 + write_or_die(p->out, buffer, size);
1217 + return size;
1218 +}
1219 +
1220 +/* Issues a request to the HTTP server configured in `p` */
1221 +static int proxy_request(struct proxy_state *p)
1222 +{
1223 + struct active_request_slot *slot;
1224 +
1225 + slot = get_active_slot();
1226 +
1227 + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0);
1228 + curl_easy_setopt(slot->curl, CURLOPT_POST, 1);
1229 + curl_easy_setopt(slot->curl, CURLOPT_URL, p->service_url);
1230 + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, p->headers);
1231 +
1232 + /* Setup function to read request from client */
1233 + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, proxy_in);
1234 + curl_easy_setopt(slot->curl, CURLOPT_READDATA, p);
1235 +
1236 + /* Setup function to write server response to client */
1237 + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, proxy_out);
1238 + curl_easy_setopt(slot->curl, CURLOPT_WRITEDATA, p);
1239 +
1240 + if (run_slot(slot, NULL) != HTTP_OK)
1241 + return -1;
1242 +
1243 + return 0;
1244 +}
1245 +
1246 +static int stateless_connect(const char *service_name)
1247 +{
1248 + struct discovery *discover;
1249 + struct proxy_state p;
1250 +
1251 + /*
1252 + * Run the info/refs request and see if the server supports protocol
1253 + * v2. If and only if the server supports v2 can we successfully
1254 + * establish a stateless connection, otherwise we need to tell the
1255 + * client to fallback to using other transport helper functions to
1256 + * complete their request.
1257 + */
1258 + discover = discover_refs(service_name, 0);
1259 + if (discover->version != protocol_v2) {
1260 + printf("fallback\n");
1261 + fflush(stdout);
1262 + return -1;
1263 + } else {
1264 + /* Stateless Connection established */
1265 + printf("\n");
1266 + fflush(stdout);
1267 + }
1268 +
1269 + proxy_state_init(&p, service_name, discover->version);
1270 +
1271 + /*
1272 + * Dump the capability listing that we got from the server earlier
1273 + * during the info/refs request.
1274 + */
1275 + write_or_die(p.out, discover->buf, discover->len);
1276 +
1277 + /* Peek the next packet line. Until we see EOF keep sending POSTs */
1278 + while (packet_reader_peek(&p.reader) != PACKET_READ_EOF) {
1279 + if (proxy_request(&p)) {
1280 + /* We would have an err here */
1281 + break;
1282 + }
1283 + }
1284 +
1285 + proxy_state_clear(&p);
1286 + return 0;
1287 +}
1288 +
1289 int cmd_main(int argc, const char **argv)
1290 {
1291 struct strbuf buf = STRBUF_INIT;
@@ -1153,12 +1354,16 @@ int cmd_main(int argc, const char **argv)
1354 fflush(stdout);
1355
1356 } else if (!strcmp(buf.buf, "capabilities")) {
1357 + printf("stateless-connect\n");
1358 printf("fetch\n");
1359 printf("option\n");
1360 printf("push\n");
1361 printf("check-connectivity\n");
1362 printf("\n");
1363 fflush(stdout);
1364 + } else if (skip_prefix(buf.buf, "stateless-connect ", &arg)) {
1365 + if (!stateless_connect(arg))
1366 + break;
1367 } else {
1368 error("remote-curl: unknown command '%s' from git", buf.buf);
1369 return 1;
t/t5702-protocol-v2.sh
+45
@@ -201,4 +201,49 @@ test_expect_success 'ref advertisment is filtered during fetch using protocol v2
201 ! grep "refs/tags/three" log
202 '
203
204 +# Test protocol v2 with 'http://' transport
205 +#
206 +. "$TEST_DIRECTORY"/lib-httpd.sh
207 +start_httpd
208 +
209 +test_expect_success 'create repo to be served by http:// transport' '
210 + git init "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
211 + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config http.receivepack true &&
212 + test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" one
213 +'
214 +
215 +test_expect_success 'clone with http:// using protocol v2' '
216 + test_when_finished "rm -f log" &&
217 +
218 + GIT_TRACE_PACKET="$(pwd)/log" GIT_TRACE_CURL="$(pwd)/log" git -c protocol.version=2 \
219 + clone "$HTTPD_URL/smart/http_parent" http_child &&
220 +
221 + git -C http_child log -1 --format=%s >actual &&
222 + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" log -1 --format=%s >expect &&
223 + test_cmp expect actual &&
224 +
225 + # Client requested to use protocol v2
226 + grep "Git-Protocol: version=2" log &&
227 + # Server responded using protocol v2
228 + grep "git< version 2" log
229 +'
230 +
231 +test_expect_success 'fetch with http:// using protocol v2' '
232 + test_when_finished "rm -f log" &&
233 +
234 + test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" two &&
235 +
236 + GIT_TRACE_PACKET="$(pwd)/log" git -C http_child -c protocol.version=2 \
237 + fetch &&
238 +
239 + git -C http_child log -1 --format=%s origin/master >actual &&
240 + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" log -1 --format=%s >expect &&
241 + test_cmp expect actual &&
242 +
243 + # Server responded using protocol v2
244 + grep "git< version 2" log
245 +'
246 +
247 +stop_httpd
248 +
249 test_done