]> git.ipfire.org Git - thirdparty/git.git/commitdiff
http: do not ignore proxy path
authorRyan Hendrickson <ryan.hendrickson@alum.mit.edu>
Fri, 2 Aug 2024 05:20:07 +0000 (05:20 +0000)
committerJunio C Hamano <gitster@pobox.com>
Fri, 2 Aug 2024 15:30:08 +0000 (08:30 -0700)
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>
Documentation/config/http.txt
http.c
t/socks4-proxy.pl [new file with mode: 0644]
t/t5564-http-proxy.sh

index 162b33fc52f9672cc14808c77e7e90e53c2da889..a14371b5c96a51d538ba37a4331c840facd37dc4 100644 (file)
@@ -5,8 +5,8 @@ http.proxy::
        proxy string with a user name but no password, in which case git will
        attempt to acquire one in the same way it does for other credentials. See
        linkgit:gitcredentials[7] for more information. The syntax thus is
-       '[protocol://][user[:password]@]proxyhost[:port]'. This can be overridden
-       on a per-remote basis; see remote.<name>.proxy
+       '[protocol://][user[:password]@]proxyhost[:port][/path]'. This can be
+       overridden on a per-remote basis; see remote.<name>.proxy
 +
 Any proxy, however configured, must be completely transparent and must not
 modify, transform, or buffer the request or response in any way.  Proxies which
diff --git a/http.c b/http.c
index 623ed234891f44dbc9b767ada98a854320921c30..6c6cc5c822ad5ae741a9902aad35aa94cef2260d 100644 (file)
--- a/http.c
+++ b/http.c
@@ -1227,6 +1227,8 @@ static CURL *get_curl_handle(void)
                 */
                curl_easy_setopt(result, CURLOPT_PROXY, "");
        } else if (curl_http_proxy) {
+               struct strbuf proxy = STRBUF_INIT;
+
                if (starts_with(curl_http_proxy, "socks5h"))
                        curl_easy_setopt(result,
                                CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
@@ -1265,7 +1267,27 @@ static CURL *get_curl_handle(void)
                if (!proxy_auth.host)
                        die("Invalid proxy URL '%s'", curl_http_proxy);
 
-               curl_easy_setopt(result, CURLOPT_PROXY, proxy_auth.host);
+               strbuf_addstr(&proxy, proxy_auth.host);
+               if (proxy_auth.path) {
+                       curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW);
+
+                       if (ver->version_num < 0x075400)
+                               die("libcurl 7.84 or later is required to support paths in proxy URLs");
+
+                       if (!starts_with(proxy_auth.protocol, "socks"))
+                               die("Invalid proxy URL '%s': only SOCKS proxies support paths",
+                                   curl_http_proxy);
+
+                       if (strcasecmp(proxy_auth.host, "localhost"))
+                               die("Invalid proxy URL '%s': host must be localhost if a path is present",
+                                   curl_http_proxy);
+
+                       strbuf_addch(&proxy, '/');
+                       strbuf_add_percentencode(&proxy, proxy_auth.path, 0);
+               }
+               curl_easy_setopt(result, CURLOPT_PROXY, proxy.buf);
+               strbuf_release(&proxy);
+
                var_override(&curl_no_proxy, getenv("NO_PROXY"));
                var_override(&curl_no_proxy, getenv("no_proxy"));
                curl_easy_setopt(result, CURLOPT_NOPROXY, curl_no_proxy);
diff --git a/t/socks4-proxy.pl b/t/socks4-proxy.pl
new file mode 100644 (file)
index 0000000..4c3a35c
--- /dev/null
@@ -0,0 +1,48 @@
+use strict;
+use IO::Select;
+use IO::Socket::UNIX;
+use IO::Socket::INET;
+
+my $path = shift;
+
+unlink($path);
+my $server = IO::Socket::UNIX->new(Listen => 1, Local => $path)
+       or die "unable to listen on $path: $!";
+
+$| = 1;
+print "ready\n";
+
+while (my $client = $server->accept()) {
+       sysread $client, my $buf, 8;
+       my ($version, $cmd, $port, $ip) = unpack 'CCnN', $buf;
+       next unless $version == 4; # socks4
+       next unless $cmd == 1; # TCP stream connection
+
+       # skip NUL-terminated id
+       while (sysread $client, my $char, 1) {
+               last unless ord($char);
+       }
+
+       # version(0), reply(5a == granted), port (ignored), ip (ignored)
+       syswrite $client, "\x00\x5a\x00\x00\x00\x00\x00\x00";
+
+       my $remote = IO::Socket::INET->new(PeerHost => $ip, PeerPort => $port)
+               or die "unable to connect to $ip/$port: $!";
+
+       my $io = IO::Select->new($client, $remote);
+       while ($io->count) {
+               for my $fh ($io->can_read(0)) {
+                       for my $pair ([$client, $remote], [$remote, $client]) {
+                               my ($from, $to) = @$pair;
+                               next unless $fh == $from;
+
+                               my $r = sysread $from, my $buf, 1024;
+                               if (!defined $r || $r <= 0) {
+                                       $io->remove($from);
+                                       next;
+                               }
+                               syswrite $to, $buf;
+                       }
+               }
+       }
+}
index bb35b87071d93dd5eacfeac67034f198dbf6a449..4aef99bc28a46e729e153526f7b6fa34b3bf9da3 100755 (executable)
@@ -39,4 +39,59 @@ test_expect_success 'clone can prompt for proxy password' '
        expect_askpass pass proxuser
 '
 
+start_socks() {
+       mkfifo socks_output &&
+       {
+               "$PERL_PATH" "$TEST_DIRECTORY/socks4-proxy.pl" "$1" >socks_output &
+               echo $! > "$TRASH_DIRECTORY/socks.pid"
+       } &&
+       read line <socks_output &&
+       test "$line" = ready
+}
+
+# The %30 tests that the correct amount of percent-encoding is applied to the
+# proxy string passed to curl.
+test_lazy_prereq SOCKS_PROXY '
+       test_have_prereq PERL &&
+       start_socks "$TRASH_DIRECTORY/%30.sock"
+'
+
+test_atexit '
+       test ! -e "$TRASH_DIRECTORY/socks.pid" ||
+       kill "$(cat "$TRASH_DIRECTORY/socks.pid")"
+'
+
+# The below tests morally ought to be gated on a prerequisite that Git is
+# linked with a libcurl that supports Unix socket paths for proxies (7.84 or
+# later), but this is not easy to test right now. Instead, we || the tests with
+# this function.
+old_libcurl_error() {
+       grep -Fx "fatal: libcurl 7.84 or later is required to support paths in proxy URLs" "$1"
+}
+
+test_expect_success SOCKS_PROXY 'clone via Unix socket' '
+       test_when_finished "rm -rf clone" &&
+       test_config_global http.proxy "socks4://localhost$PWD/%2530.sock" && {
+               {
+                       GIT_TRACE_CURL=$PWD/trace git clone "$HTTPD_URL/smart/repo.git" clone 2>err &&
+                       grep -i "SOCKS4 request granted" trace
+               } ||
+               old_libcurl_error err
+       }
+'
+
+test_expect_success 'Unix socket requires socks*:' - <<\EOT
+       ! git clone -c http.proxy=localhost/path https://example.com/repo.git 2>err && {
+               grep -Fx "fatal: Invalid proxy URL 'localhost/path': only SOCKS proxies support paths" err ||
+               old_libcurl_error err
+       }
+EOT
+
+test_expect_success 'Unix socket requires localhost' - <<\EOT
+       ! git clone -c http.proxy=socks4://127.0.0.1/path https://example.com/repo.git 2>err && {
+               grep -Fx "fatal: Invalid proxy URL 'socks4://127.0.0.1/path': host must be localhost if a path is present" err ||
+               old_libcurl_error err
+       }
+EOT
+
 test_done