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