Commit c32afe91e2
Verified · cmc
Layout: unified · split
internal/gitpin/gitpin.go +27 −2
| @@ -46,6 +46,11 @@ func Resolve(ctx context.Context, lookup Lookup, raw string, allowLocal bool) (R | ||
| 46 | 46 | if host == "" { |
| 47 | 47 | return Remote{}, fmt.Errorf("URL has no host") |
| 48 | 48 | } |
| 49 | if net.ParseIP(host) == nil && numericHost(host) { | |
| 50 | // 127.1, 2130706433 and 0x7f.1 are loopback to curl's parser | |
| 51 | // but not to Go's; refuse rather than leave them to a resolver. | |
| 52 | return Remote{}, fmt.Errorf("host %q is a numeric address in a form other than dotted decimal; write it as a.b.c.d", host) | |
| 53 | } | |
| 49 | 54 | ips, err := lookup(ctx, host) |
| 50 | 55 | if err != nil { |
| 51 | 56 | return Remote{}, fmt.Errorf("resolving %s: %w", host, err) |
| @@ -60,8 +65,24 @@ func Resolve(ctx context.Context, lookup Lookup, raw string, allowLocal bool) (R | ||
| 60 | 65 | return Remote{URL: u, IPs: ips}, nil |
| 61 | 66 | } |
| 62 | 67 | |
| 68 | // numericHost reports whether every label of host is a decimal, octal | |
| 69 | // or hex number, the shapes inet_aton reads as an IPv4 address. | |
| 70 | func numericHost(host string) bool { | |
| 71 | for _, label := range strings.Split(strings.TrimSuffix(host, "."), ".") { | |
| 72 | digits, base := label, "0123456789" | |
| 73 | if rest, ok := strings.CutPrefix(strings.ToLower(label), "0x"); ok { | |
| 74 | digits, base = rest, "0123456789abcdef" | |
| 75 | } | |
| 76 | if strings.Trim(strings.ToLower(digits), base) != "" || label == "" { | |
| 77 | return false | |
| 78 | } | |
| 79 | } | |
| 80 | return true | |
| 81 | } | |
| 82 | ||
| 63 | 83 | // Args are git's leading -c options for r: curl's resolve list pins |
| 64 | // the host to the checked addresses, and with redirects off a server | |
| 84 | // the host, and any other name on the same port, to the checked | |
| 85 | // addresses, and with redirects off a server | |
| 65 | 86 | // cannot send git on to a host nobody checked. An address literal |
| 66 | 87 | // needs no pin. |
| 67 | 88 | func (r Remote) Args() []string { |
| @@ -85,7 +106,11 @@ func (r Remote) Args() []string { | ||
| 85 | 106 | addrs[i] = ip.String() |
| 86 | 107 | } |
| 87 | 108 | } |
| 88 | return append(args, "-c", "http.curloptResolve="+host+":"+port+":"+strings.Join(addrs, ",")) | |
| 109 | pinned := port + ":" + strings.Join(addrs, ",") | |
| 110 | // The wildcard entry catches a lookup under any other spelling of | |
| 111 | // the host, so it too lands on the checked addresses. | |
| 112 | return append(args, "-c", "http.curloptResolve="+host+":"+pinned, | |
| 113 | "-c", "http.curloptResolve=*:"+pinned) | |
| 89 | 114 | } |
| 90 | 115 | |
| 91 | 116 | // Env is git's whole environment for a pinned remote. No system or |
internal/gitpin/gitpin_test.go +24 −2
| @@ -46,16 +46,38 @@ func TestResolve(t *testing.T) { | ||
| 46 | 46 | } |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | // Numeric hosts other than a dotted quad are refused before any lookup: | |
| 50 | // curl reads them as addresses the check never saw. | |
| 51 | func TestResolveRefusesOddNumericHosts(t *testing.T) { | |
| 52 | never := func(_ context.Context, host string) ([]net.IP, error) { | |
| 53 | t.Fatalf("looked up %s", host) | |
| 54 | return nil, nil | |
| 55 | } | |
| 56 | for _, host := range []string{"127.1", "2130706433", "0x7f.1", "0x7F000001", "017700000001", "127.0.0.01", "127.0.0.1."} { | |
| 57 | if _, err := Resolve(context.Background(), never, "http://"+host+"/x.git", true); err == nil || !strings.Contains(err.Error(), "numeric address") { | |
| 58 | t.Errorf("%s: %v", host, err) | |
| 59 | } | |
| 60 | } | |
| 61 | // Names with a numeric label, and real literals, still pass. | |
| 62 | for _, host := range []string{"1.example", "0x7f.example", "203.0.113.5", "[2001:db8::1]"} { | |
| 63 | if _, err := Resolve(context.Background(), answer("203.0.113.5"), "http://"+host+"/x.git", false); err != nil { | |
| 64 | t.Errorf("%s: %v", host, err) | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 49 | 69 | func TestArgs(t *testing.T) { |
| 50 | 70 | u, _ := url.Parse("https://git.example/x.git") |
| 51 | 71 | got := Remote{u, []net.IP{net.ParseIP("203.0.113.5"), net.ParseIP("2001:db8::1")}}.Args() |
| 52 | 72 | want := []string{"-c", "http.followRedirects=false", |
| 53 | "-c", "http.curloptResolve=git.example:443:203.0.113.5,[2001:db8::1]"} | |
| 73 | "-c", "http.curloptResolve=git.example:443:203.0.113.5,[2001:db8::1]", | |
| 74 | "-c", "http.curloptResolve=*:443:203.0.113.5,[2001:db8::1]"} | |
| 54 | 75 | if !slices.Equal(got, want) { |
| 55 | 76 | t.Fatalf("https: %q", got) |
| 56 | 77 | } |
| 57 | 78 | u, _ = url.Parse("http://git.example:8080/x.git") |
| 58 | if got := (Remote{u, []net.IP{net.ParseIP("203.0.113.5")}}).Args(); got[3] != "http.curloptResolve=git.example:8080:203.0.113.5" { | |
| 79 | if got := (Remote{u, []net.IP{net.ParseIP("203.0.113.5")}}).Args(); got[3] != "http.curloptResolve=git.example:8080:203.0.113.5" || | |
| 80 | got[5] != "http.curloptResolve=*:8080:203.0.113.5" { | |
| 59 | 81 | t.Fatalf("http with port: %q", got) |
| 60 | 82 | } |
| 61 | 83 | // An address literal is its own resolution; there is nothing to pin. |