internal/git/gitcmd/command_resolve.go (71 lines of code) (raw):

package gitcmd import ( "fmt" "net" "net/url" "strings" ) // GetURLAndResolveConfig parses the given repository's URL and resolved address to generate // the modified URL and configuration to avoid DNS rebinding. // // In Git v2.37.0 we added the functionality for `http.curloptResolve` which like its // curl counterpart when provided with a `HOST:PORT:ADDRESS` value, uses the IP Address provided // directly for the given HOST:PORT combination for HTTP/HTTPS protocols, without requiring // DNS resolution. // // This functions currently does the following operations: // // - Git Protocol: Replaces the hostname with the resolved IP address. // - SSH Protocol: Replaces the hostname with the resolved IP address (supports both the regular syntax // `ssh://[user@]server/project.git` and scp-like syntax `[user@]server:project.git`). // - HTTP/HTTPS Protocol: Keeps the URL as is, but adds the `http.curloptResolve` flag. // // SideNote: We cannot replace the hostname with IP in HTTPS protocol because the protocol // demands the hostname to be present, as it is required for the SSL verification. func GetURLAndResolveConfig(remoteURL string, resolvedAddress string) (string, []ConfigPair, error) { if remoteURL == "" { return "", nil, fmt.Errorf("URL is empty") } if resolvedAddress == "" { return "", nil, fmt.Errorf("resolved address is empty") } resolvedIP := net.ParseIP(resolvedAddress) if resolvedIP == nil { return "", nil, fmt.Errorf("resolved address has invalid IPv4/IPv6 address") } switch { case strings.HasPrefix(remoteURL, "http://"), strings.HasPrefix(remoteURL, "https://"), strings.HasPrefix(remoteURL, "git://"): return getURLAndResolveConfigForURL(remoteURL, resolvedAddress) case strings.HasPrefix(remoteURL, "ssh://"): return getURLAndResolveConfigForSSH(remoteURL, resolvedAddress) default: return getURLAndResolveConfigForSCP(remoteURL, resolvedAddress) } } func getURLAndResolveConfigForSSH(remoteURL, resolvedAddress string) (string, []ConfigPair, error) { u, err := url.ParseRequestURI(remoteURL) if err != nil { return "", nil, fmt.Errorf("couldn't parse remoteURL: %w", err) } u.Host = resolvedAddress return u.String(), nil, nil } func getURLAndResolveConfigForSCP(remoteURL, resolvedAddress string) (string, []ConfigPair, error) { hostAndPath := strings.SplitN(remoteURL, ":", 2) if len(hostAndPath) != 2 { return "", nil, fmt.Errorf("invalid protocol/URL encountered: %s", remoteURL) } if strings.Contains(hostAndPath[0], "/") { return "", nil, fmt.Errorf("SSH URLs with '/' before colon are unsupported") } var userPrefix string if userAndHost := strings.SplitAfterN(remoteURL, "@", 2); len(userAndHost) > 1 { userPrefix = userAndHost[0] } return fmt.Sprintf("%s%s:%s", userPrefix, resolvedAddress, hostAndPath[1]), nil, nil } func getURLAndResolveConfigForURL(remoteURL, resolvedAddress string) (string, []ConfigPair, error) { u, err := url.ParseRequestURI(remoteURL) if err != nil { return "", nil, fmt.Errorf("couldn't parse remoteURL: %w", err) } port := u.Port() if port == "" { switch u.Scheme { case "http": port = "80" case "https": port = "443" case "git": port = "9418" default: return "", nil, fmt.Errorf("unknown schema provided: %s", u.Scheme) } } return remoteURL, []ConfigPair{ {Key: "http.curloptResolve", Value: fmt.Sprintf("%s:%s:%s", u.Hostname(), port, resolvedAddress)}, }, nil }