Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKarthik Nayak <knayak@gitlab.com>2022-09-28 20:06:52 +0300
committerJohn Cai <jcai@gitlab.com>2022-11-02 21:40:05 +0300
commitbe7cbb60434349041c173a671ad6497d4428e555 (patch)
treea8434c66de6b9456e84e22eeb774d392e74795ba
parentcbdc52900053c3dfca8871b42a9fc3ad428a12cc (diff)
git: Add `GetURLAndCurloptResolveConfig()`
To facilitate the creation of the ConfigPair for the `http.curloptResolve` flag or modification of the URL for avoiding DNS rebinding we introduce the `GetURLAndCurloptResolveConfig()` function. This function takes in the remoteURL and the resolvedAddress and then provides the modified URL and configPair to be passed onto Git. For HTTP/HTTPS URLs we leave the remoteURL as is and use the `http.curloptResolve` flag which was introduced in Git. This is done by providing a new ConfigPair with the value in the format of `HOST:PORT:ADDRESS[,ADDRESS]`. For SSH/Git protocol URLs we simply replace the hostname with the provided resolved address. The function does basic validation of the provided remoteURL and resolved address and throws errors if any validation failed.
-rw-r--r--internal/git/command_resolve.go105
-rw-r--r--internal/git/command_resolve_test.go217
2 files changed, 322 insertions, 0 deletions
diff --git a/internal/git/command_resolve.go b/internal/git/command_resolve.go
new file mode 100644
index 000000000..fc7d334b0
--- /dev/null
+++ b/internal/git/command_resolve.go
@@ -0,0 +1,105 @@
+package git
+
+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", port, resolvedAddress)},
+ }, nil
+}
diff --git a/internal/git/command_resolve_test.go b/internal/git/command_resolve_test.go
new file mode 100644
index 000000000..b3d059519
--- /dev/null
+++ b/internal/git/command_resolve_test.go
@@ -0,0 +1,217 @@
+package git
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetUrlAndResolveConfig(t *testing.T) {
+ t.Parallel()
+
+ type args struct {
+ remoteURL string
+ resolvedAddress string
+ }
+ tests := []struct {
+ desc string
+ args args
+ expectedURL string
+ expectedConfigPair []ConfigPair
+ expectedErrString string
+ }{
+ {
+ desc: "No remote URL provided",
+ args: args{
+ remoteURL: "",
+ resolvedAddress: "192.0.0.1",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "URL is empty",
+ },
+ {
+ desc: "No resolved address provided",
+ args: args{
+ remoteURL: "www.gitlab.com",
+ resolvedAddress: "",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "resolved address is empty",
+ },
+ {
+ desc: "resolved address has malformed IP",
+ args: args{
+ remoteURL: "www.gitlab.com",
+ resolvedAddress: "abcd",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "resolved address has invalid IPv4/IPv6 address",
+ },
+ {
+ desc: "resolved address has malformed IP",
+ args: args{
+ remoteURL: "www.gitlab.com",
+ resolvedAddress: "1922.23.2323.2323",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "resolved address has invalid IPv4/IPv6 address",
+ },
+ {
+ desc: "bad URL format",
+ args: args{
+ remoteURL: "http//foo/bar",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "invalid protocol/URL encountered: http//foo/bar",
+ },
+ {
+ desc: "valid http format",
+ args: args{
+ remoteURL: "http://gitlab.com/gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "http://gitlab.com/gitlab-org/gitaly.git",
+ expectedConfigPair: []ConfigPair{{Key: "http.curloptResolve", Value: "*:80:192.168.0.1"}},
+ expectedErrString: "",
+ },
+ {
+ desc: "valid https format",
+ args: args{
+ remoteURL: "https://gitlab.com/gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "https://gitlab.com/gitlab-org/gitaly.git",
+ expectedConfigPair: []ConfigPair{{Key: "http.curloptResolve", Value: "*:443:192.168.0.1"}},
+ expectedErrString: "",
+ },
+ {
+ desc: "valid https format with port",
+ args: args{
+ remoteURL: "https://gitlab.com:1234/gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "https://gitlab.com:1234/gitlab-org/gitaly.git",
+ expectedConfigPair: []ConfigPair{{Key: "http.curloptResolve", Value: "*:1234:192.168.0.1"}},
+ expectedErrString: "",
+ },
+ {
+ desc: "valid ssh format",
+ args: args{
+ remoteURL: "ssh://user@gitlab.com/gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "ssh://user@192.168.0.1/gitlab-org/gitaly.git",
+ expectedConfigPair: nil,
+ expectedErrString: "",
+ },
+ {
+ desc: "valid ssh format without username",
+ args: args{
+ remoteURL: "ssh://gitlab.com/gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "ssh://192.168.0.1/gitlab-org/gitaly.git",
+ expectedConfigPair: nil,
+ expectedErrString: "",
+ },
+ {
+ desc: "invalid ssh format",
+ args: args{
+ remoteURL: "ssh://foo" + string(rune(0x7f)),
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "couldn't parse remoteURL",
+ },
+ {
+ desc: "valid ssh format (scp-like) without prefix",
+ args: args{
+ remoteURL: "user@gitlab.com:gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "user@192.168.0.1:gitlab-org/gitaly.git",
+ expectedConfigPair: nil,
+ expectedErrString: "",
+ },
+ {
+ desc: "valid ssh format (scp-like) without prefix without user",
+ args: args{
+ remoteURL: "gitlab.com:gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "192.168.0.1:gitlab-org/gitaly.git",
+ expectedConfigPair: nil,
+ expectedErrString: "",
+ },
+ {
+ desc: "invalid format (shouldn't fallback to other protocol)",
+ args: args{
+ remoteURL: "gitlab.com/gitlab-org/gitaly.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "invalid protocol/URL encountered: gitlab.com/gitlab-org/gitaly.git",
+ },
+ {
+ desc: "invalid git (SCP) url provided",
+ args: args{
+ remoteURL: "git@gitlab.com/foo/bar",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "invalid protocol/URL encountered: git@gitlab.com/foo/bar",
+ },
+ {
+ desc: "valid git (SCP) url provided",
+ args: args{
+ remoteURL: "git@gitlab.com:gitlab-org/security/gitlab.git",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "git@192.168.0.1:gitlab-org/security/gitlab.git",
+ expectedConfigPair: nil,
+ expectedErrString: "",
+ },
+ {
+ desc: "valid git url provided",
+ args: args{
+ remoteURL: "git://www.gitlab.com/foo/bar",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "git://www.gitlab.com/foo/bar",
+ expectedConfigPair: []ConfigPair{{Key: "http.curloptResolve", Value: "*:9418:192.168.0.1"}},
+ expectedErrString: "",
+ },
+ {
+ desc: "ssh url with slash before colon",
+ args: args{
+ remoteURL: "foo@bar/goo.com:/abc/def",
+ resolvedAddress: "192.168.0.1",
+ },
+ expectedURL: "",
+ expectedConfigPair: nil,
+ expectedErrString: "SSH URLs with '/' before colon are unsupported",
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.desc, func(t *testing.T) {
+ url, configPair, err := GetURLAndResolveConfig(tc.args.remoteURL, tc.args.resolvedAddress)
+
+ if tc.expectedErrString != "" {
+ require.Error(t, err, tc.expectedErrString)
+ } else {
+ require.NoError(t, err)
+ }
+ require.Equal(t, tc.expectedURL, url)
+ require.Equal(t, tc.expectedConfigPair, configPair)
+ })
+ }
+}