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 | if host == "" { | 46 | if host == "" { |
| 47 | return Remote{}, fmt.Errorf("URL has no host") | 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 | ips, err := lookup(ctx, host) | 54 | ips, err := lookup(ctx, host) |
| 50 | if err != nil { | 55 | if err != nil { |
| 51 | return Remote{}, fmt.Errorf("resolving %s: %w", host, err) | 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 | return Remote{URL: u, IPs: ips}, nil | 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 | // Args are git's leading -c options for r: curl's resolve list pins | 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 | // cannot send git on to a host nobody checked. An address literal | 86 | // cannot send git on to a host nobody checked. An address literal |
| 66 | // needs no pin. | 87 | // needs no pin. |
| 67 | func (r Remote) Args() []string { | 88 | func (r Remote) Args() []string { |
| @@ -85,7 +106,11 @@ func (r Remote) Args() []string { | |||
| 85 | addrs[i] = ip.String() | 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 | // Env is git's whole environment for a pinned remote. No system or | 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 | func TestArgs(t *testing.T) { | 69 | func TestArgs(t *testing.T) { |
| 50 | u, _ := url.Parse("https://git.example/x.git") | 70 | u, _ := url.Parse("https://git.example/x.git") |
| 51 | got := Remote{u, []net.IP{net.ParseIP("203.0.113.5"), net.ParseIP("2001:db8::1")}}.Args() | 71 | got := Remote{u, []net.IP{net.ParseIP("203.0.113.5"), net.ParseIP("2001:db8::1")}}.Args() |
| 52 | want := []string{"-c", "http.followRedirects=false", | 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 | if !slices.Equal(got, want) { | 75 | if !slices.Equal(got, want) { |
| 55 | t.Fatalf("https: %q", got) | 76 | t.Fatalf("https: %q", got) |
| 56 | } | 77 | } |
| 57 | u, _ = url.Parse("http://git.example:8080/x.git") | 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 | t.Fatalf("http with port: %q", got) | 81 | t.Fatalf("http with port: %q", got) |
| 60 | } | 82 | } |
| 61 | // An address literal is its own resolution; there is nothing to pin. | 83 | // An address literal is its own resolution; there is nothing to pin. |