http: do not ignore proxy path

The documentation for `http.proxy` describes that option, and the environment variables it overrides, as supporting "the syntax understood by curl". curl allows SOCKS proxies to use a path to a Unix domain socket, like `socks5h://localhost/path/to/socket.sock`. Git should therefore include, if present, the path part of the proxy URL in what it passes to libcurl. Co-authored-by: Jeff King <peff@peff.net> Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Ryan Hendrickson <ryan.hendrickson@alum.mit.edu> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ryan Hendrickson committed Aug 2, 2024 at 05:20 UTC 0ca365c2ed48084974c7081bdfe3189094a2b993
4 files changed +128 -3
Documentation/config/http.txt
+2 -2
@@ -5,8 +5,8 @@ http.proxy::
5 proxy string with a user name but no password, in which case git will
6 attempt to acquire one in the same way it does for other credentials. See
7 linkgit:gitcredentials[7] for more information. The syntax thus is
8 - '[protocol://][user[:password]@]proxyhost[:port]'. This can be overridden
9 - on a per-remote basis; see remote.<name>.proxy
8 + '[protocol://][user[:password]@]proxyhost[:port][/path]'. This can be
9 + overridden on a per-remote basis; see remote.<name>.proxy
10 +
11 Any proxy, however configured, must be completely transparent and must not
12 modify, transform, or buffer the request or response in any way. Proxies which
http.c
+23 -1
@@ -1227,6 +1227,8 @@ static CURL *get_curl_handle(void)
1227 */
1228 curl_easy_setopt(result, CURLOPT_PROXY, "");
1229 } else if (curl_http_proxy) {
1230 + struct strbuf proxy = STRBUF_INIT;
1231 +
1232 if (starts_with(curl_http_proxy, "socks5h"))
1233 curl_easy_setopt(result,
1234 CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
@@ -1265,7 +1267,27 @@ static CURL *get_curl_handle(void)
1267 if (!proxy_auth.host)
1268 die("Invalid proxy URL '%s'", curl_http_proxy);
1269
1268 - curl_easy_setopt(result, CURLOPT_PROXY, proxy_auth.host);
1270 + strbuf_addstr(&proxy, proxy_auth.host);
1271 + if (proxy_auth.path) {
1272 + curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW);
1273 +
1274 + if (ver->version_num < 0x075400)
1275 + die("libcurl 7.84 or later is required to support paths in proxy URLs");
1276 +
1277 + if (!starts_with(proxy_auth.protocol, "socks"))
1278 + die("Invalid proxy URL '%s': only SOCKS proxies support paths",
1279 + curl_http_proxy);
1280 +
1281 + if (strcasecmp(proxy_auth.host, "localhost"))
1282 + die("Invalid proxy URL '%s': host must be localhost if a path is present",
1283 + curl_http_proxy);
1284 +
1285 + strbuf_addch(&proxy, '/');
1286 + strbuf_add_percentencode(&proxy, proxy_auth.path, 0);
1287 + }
1288 + curl_easy_setopt(result, CURLOPT_PROXY, proxy.buf);
1289 + strbuf_release(&proxy);
1290 +
1291 var_override(&curl_no_proxy, getenv("NO_PROXY"));
1292 var_override(&curl_no_proxy, getenv("no_proxy"));
1293 curl_easy_setopt(result, CURLOPT_NOPROXY, curl_no_proxy);
t/socks4-proxy.pl new
+48
@@ -0,0 +1,48 @@
1 +use strict;
2 +use IO::Select;
3 +use IO::Socket::UNIX;
4 +use IO::Socket::INET;
5 +
6 +my $path = shift;
7 +
8 +unlink($path);
9 +my $server = IO::Socket::UNIX->new(Listen => 1, Local => $path)
10 + or die "unable to listen on $path: $!";
11 +
12 +$| = 1;
13 +print "ready\n";
14 +
15 +while (my $client = $server->accept()) {
16 + sysread $client, my $buf, 8;
17 + my ($version, $cmd, $port, $ip) = unpack 'CCnN', $buf;
18 + next unless $version == 4; # socks4
19 + next unless $cmd == 1; # TCP stream connection
20 +
21 + # skip NUL-terminated id
22 + while (sysread $client, my $char, 1) {
23 + last unless ord($char);
24 + }
25 +
26 + # version(0), reply(5a == granted), port (ignored), ip (ignored)
27 + syswrite $client, "\x00\x5a\x00\x00\x00\x00\x00\x00";
28 +
29 + my $remote = IO::Socket::INET->new(PeerHost => $ip, PeerPort => $port)
30 + or die "unable to connect to $ip/$port: $!";
31 +
32 + my $io = IO::Select->new($client, $remote);
33 + while ($io->count) {
34 + for my $fh ($io->can_read(0)) {
35 + for my $pair ([$client, $remote], [$remote, $client]) {
36 + my ($from, $to) = @$pair;
37 + next unless $fh == $from;
38 +
39 + my $r = sysread $from, my $buf, 1024;
40 + if (!defined $r || $r <= 0) {
41 + $io->remove($from);
42 + next;
43 + }
44 + syswrite $to, $buf;
45 + }
46 + }
47 + }
48 +}
t/t5564-http-proxy.sh
+55
@@ -39,4 +39,59 @@ test_expect_success 'clone can prompt for proxy password' '
39 expect_askpass pass proxuser
40 '
41
42 +start_socks() {
43 + mkfifo socks_output &&
44 + {
45 + "$PERL_PATH" "$TEST_DIRECTORY/socks4-proxy.pl" "$1" >socks_output &
46 + echo $! > "$TRASH_DIRECTORY/socks.pid"
47 + } &&
48 + read line <socks_output &&
49 + test "$line" = ready
50 +}
51 +
52 +# The %30 tests that the correct amount of percent-encoding is applied to the
53 +# proxy string passed to curl.
54 +test_lazy_prereq SOCKS_PROXY '
55 + test_have_prereq PERL &&
56 + start_socks "$TRASH_DIRECTORY/%30.sock"
57 +'
58 +
59 +test_atexit '
60 + test ! -e "$TRASH_DIRECTORY/socks.pid" ||
61 + kill "$(cat "$TRASH_DIRECTORY/socks.pid")"
62 +'
63 +
64 +# The below tests morally ought to be gated on a prerequisite that Git is
65 +# linked with a libcurl that supports Unix socket paths for proxies (7.84 or
66 +# later), but this is not easy to test right now. Instead, we || the tests with
67 +# this function.
68 +old_libcurl_error() {
69 + grep -Fx "fatal: libcurl 7.84 or later is required to support paths in proxy URLs" "$1"
70 +}
71 +
72 +test_expect_success SOCKS_PROXY 'clone via Unix socket' '
73 + test_when_finished "rm -rf clone" &&
74 + test_config_global http.proxy "socks4://localhost$PWD/%2530.sock" && {
75 + {
76 + GIT_TRACE_CURL=$PWD/trace git clone "$HTTPD_URL/smart/repo.git" clone 2>err &&
77 + grep -i "SOCKS4 request granted" trace
78 + } ||
79 + old_libcurl_error err
80 + }
81 +'
82 +
83 +test_expect_success 'Unix socket requires socks*:' - <<\EOT
84 + ! git clone -c http.proxy=localhost/path https://example.com/repo.git 2>err && {
85 + grep -Fx "fatal: Invalid proxy URL 'localhost/path': only SOCKS proxies support paths" err ||
86 + old_libcurl_error err
87 + }
88 +EOT
89 +
90 +test_expect_success 'Unix socket requires localhost' - <<\EOT
91 + ! git clone -c http.proxy=socks4://127.0.0.1/path https://example.com/repo.git 2>err && {
92 + grep -Fx "fatal: Invalid proxy URL 'socks4://127.0.0.1/path': host must be localhost if a path is present" err ||
93 + old_libcurl_error err
94 + }
95 +EOT
96 +
97 test_done