Commit ae2edb026f
Verified · cmc
Layout: unified · split
internal/mirror/mirror.go +7 −83
| @@ -10,19 +10,16 @@ import ( | ||
| 10 | 10 | "fmt" |
| 11 | 11 | "log/slog" |
| 12 | 12 | "net" |
| 13 | "net/url" | |
| 14 | 13 | "os" |
| 15 | 14 | "os/exec" |
| 16 | 15 | "path/filepath" |
| 17 | "strconv" | |
| 18 | "strings" | |
| 19 | 16 | "time" |
| 20 | 17 | |
| 21 | 18 | "gitbay.org/gitbay/internal/config" |
| 22 | 19 | "gitbay.org/gitbay/internal/control" |
| 20 | "gitbay.org/gitbay/internal/gitpin" | |
| 23 | 21 | "gitbay.org/gitbay/internal/store" |
| 24 | 22 | "gitbay.org/gitbay/internal/toolpath" |
| 25 | "gitbay.org/gitbay/internal/webhook" | |
| 26 | 23 | ) |
| 27 | 24 | |
| 28 | 25 | const askpassScript = `#!/bin/sh |
| @@ -50,20 +47,12 @@ func New(st *store.Store, cfg config.Config) *Worker { | ||
| 50 | 47 | tick = d |
| 51 | 48 | } |
| 52 | 49 | } |
| 53 | return &Worker{St: st, Cfg: cfg, Tick: tick, | |
| 54 | Lookup: func(ctx context.Context, host string) ([]net.IP, error) { | |
| 55 | return net.DefaultResolver.LookupIP(ctx, "ip", host) | |
| 56 | }} | |
| 50 | return &Worker{St: st, Cfg: cfg, Tick: tick, Lookup: gitpin.LookupIP} | |
| 57 | 51 | } |
| 58 | 52 | |
| 59 | 53 | func (w *Worker) Run(ctx context.Context) { |
| 60 | out, err := exec.CommandContext(ctx, toolpath.Look("git"), "version").Output() | |
| 61 | if err != nil { | |
| 62 | w.gitErr = fmt.Errorf("mirrors disabled: running git version: %v", err) | |
| 63 | } else { | |
| 64 | w.gitErr = gitVersionOK(string(out)) | |
| 65 | } | |
| 66 | if w.gitErr != nil { | |
| 54 | if err := gitpin.CheckGit(ctx); err != nil { | |
| 55 | w.gitErr = fmt.Errorf("mirrors disabled: %w", err) | |
| 67 | 56 | slog.Error("mirror: not syncing", "err", w.gitErr) |
| 68 | 57 | } |
| 69 | 58 | t := time.NewTicker(w.Tick) |
| @@ -105,35 +94,18 @@ func (w *Worker) sync(m store.Mirror) error { | ||
| 105 | 94 | return err |
| 106 | 95 | } |
| 107 | 96 | dir := control.RepoDir(w.Cfg.Server.Root, repo.OwnerName, repo.Name) |
| 108 | u, err := url.Parse(m.URL) | |
| 109 | if err != nil { | |
| 110 | return err | |
| 111 | } | |
| 112 | if u.Scheme != "https" && u.Scheme != "http" { | |
| 113 | return fmt.Errorf("mirror URL scheme %q is not http or https", u.Scheme) | |
| 114 | } | |
| 115 | 97 | |
| 116 | 98 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) |
| 117 | 99 | defer cancel() |
| 118 | 100 | // The URL was checked when saved, but DNS can answer differently |
| 119 | 101 | // now. Check what it resolves to at sync time, then let git connect |
| 120 | 102 | // to exactly those addresses. |
| 121 | ips, err := w.Lookup(ctx, u.Hostname()) | |
| 103 | remote, err := gitpin.Resolve(ctx, w.Lookup, m.URL, w.Cfg.Webhooks.AllowLocal) | |
| 122 | 104 | if err != nil { |
| 123 | return fmt.Errorf("resolving %s: %w", u.Hostname(), err) | |
| 124 | } | |
| 125 | if len(ips) == 0 { | |
| 126 | // An empty resolve list would leave curl to resolve the host itself. | |
| 127 | return fmt.Errorf("%s resolves to no address", u.Hostname()) | |
| 128 | } | |
| 129 | if err := webhook.CheckAddrs(u.Hostname(), ips, w.Cfg.Webhooks.AllowLocal); err != nil { | |
| 130 | 105 | return err |
| 131 | 106 | } |
| 132 | 107 | |
| 133 | // No system or global gitconfig: a proxy, URL rewrite or redirect | |
| 134 | // setting there would take git around the pin. | |
| 135 | env := []string{"GIT_TERMINAL_PROMPT=0", "HOME=" + w.Cfg.Server.Root, | |
| 136 | "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null"} | |
| 108 | env := gitpin.Env(w.Cfg.Server.Root) | |
| 137 | 109 | if m.Token != "" { |
| 138 | 110 | askpass := filepath.Join(w.Cfg.Server.Root, "mirror-askpass.sh") |
| 139 | 111 | if err := os.WriteFile(askpass, []byte(askpassScript), 0o700); err != nil { |
| @@ -149,7 +121,7 @@ func (w *Worker) sync(m store.Mirror) error { | ||
| 149 | 121 | "GITBAY_MIRROR_TOKEN="+m.Token) |
| 150 | 122 | } |
| 151 | 123 | |
| 152 | args := append(pinArgs(u, ips), "-C", dir) | |
| 124 | args := append(remote.Args(), "-C", dir) | |
| 153 | 125 | if m.Direction == "push" { |
| 154 | 126 | // Branches and tags only: internal refs (merge-requests) stay home. |
| 155 | 127 | args = append(args, "push", "--prune", m.URL, |
| @@ -165,51 +137,3 @@ func (w *Worker) sync(m store.Mirror) error { | ||
| 165 | 137 | } |
| 166 | 138 | return nil |
| 167 | 139 | } |
| 168 | ||
| 169 | // pinArgs keeps git on the addresses just checked: curl's resolve list | |
| 170 | // pins the host, and with redirects off a server cannot send git on to | |
| 171 | // a host nobody checked. An address literal needs no pin. | |
| 172 | func pinArgs(u *url.URL, ips []net.IP) []string { | |
| 173 | args := []string{"-c", "http.followRedirects=false"} | |
| 174 | host := u.Hostname() | |
| 175 | if net.ParseIP(host) != nil { | |
| 176 | return args | |
| 177 | } | |
| 178 | port := u.Port() | |
| 179 | if port == "" { | |
| 180 | port = "443" | |
| 181 | if u.Scheme == "http" { | |
| 182 | port = "80" | |
| 183 | } | |
| 184 | } | |
| 185 | addrs := make([]string, len(ips)) | |
| 186 | for i, ip := range ips { | |
| 187 | if ip.To4() == nil { | |
| 188 | addrs[i] = "[" + ip.String() + "]" | |
| 189 | } else { | |
| 190 | addrs[i] = ip.String() | |
| 191 | } | |
| 192 | } | |
| 193 | return append(args, "-c", "http.curloptResolve="+host+":"+port+":"+strings.Join(addrs, ",")) | |
| 194 | } | |
| 195 | ||
| 196 | // gitVersionOK accepts the output of `git version` for git 2.37 or | |
| 197 | // later, the first release with http.curloptResolve. An older git | |
| 198 | // ignores the setting and would resolve the host itself. | |
| 199 | func gitVersionOK(out string) error { | |
| 200 | fields := strings.Fields(out) | |
| 201 | if len(fields) >= 3 && fields[0] == "git" && fields[1] == "version" { | |
| 202 | parts := strings.Split(fields[2], ".") | |
| 203 | if len(parts) >= 2 { | |
| 204 | major, err1 := strconv.Atoi(parts[0]) | |
| 205 | minor, err2 := strconv.Atoi(parts[1]) | |
| 206 | if err1 == nil && err2 == nil { | |
| 207 | if major > 2 || major == 2 && minor >= 37 { | |
| 208 | return nil | |
| 209 | } | |
| 210 | return fmt.Errorf("mirrors disabled: git %s is older than 2.37 and cannot pin mirror addresses", fields[2]) | |
| 211 | } | |
| 212 | } | |
| 213 | } | |
| 214 | return fmt.Errorf("mirrors disabled: cannot read git version from %q", strings.TrimSpace(out)) | |
| 215 | } | |
internal/mirror/mirror_test.go +2 −34
| @@ -15,6 +15,7 @@ import ( | ||
| 15 | 15 | |
| 16 | 16 | "gitbay.org/gitbay/internal/config" |
| 17 | 17 | "gitbay.org/gitbay/internal/control" |
| 18 | "gitbay.org/gitbay/internal/gitpin" | |
| 18 | 19 | "gitbay.org/gitbay/internal/store" |
| 19 | 20 | ) |
| 20 | 21 | |
| @@ -148,7 +149,7 @@ func TestSweepRefusesWithAnOldGit(t *testing.T) { | ||
| 148 | 149 | t.Fatal("looked up a host with an old git") |
| 149 | 150 | return nil, nil |
| 150 | 151 | }} |
| 151 | w.gitErr = gitVersionOK("git version 2.36.1") | |
| 152 | w.gitErr = gitpin.VersionOK("git version 2.36.1") | |
| 152 | 153 | w.sweep() |
| 153 | 154 | ms, err := st.ListMirrors(m.RepoID) |
| 154 | 155 | if err != nil || len(ms) != 1 { |
| @@ -159,20 +160,6 @@ func TestSweepRefusesWithAnOldGit(t *testing.T) { | ||
| 159 | 160 | } |
| 160 | 161 | } |
| 161 | 162 | |
| 162 | func TestGitVersionOK(t *testing.T) { | |
| 163 | for _, s := range []string{"git version 2.37.0", "git version 2.47.3", "git version 2.39.5 (Apple Git-154)", | |
| 164 | "git version 2.45.2.windows.1", "git version 3.0.0\n"} { | |
| 165 | if err := gitVersionOK(s); err != nil { | |
| 166 | t.Errorf("%q: %v", s, err) | |
| 167 | } | |
| 168 | } | |
| 169 | for _, s := range []string{"git version 2.36.9", "git version 1.99.0", "git version 2", "nonsense", ""} { | |
| 170 | if err := gitVersionOK(s); err == nil { | |
| 171 | t.Errorf("%q accepted", s) | |
| 172 | } | |
| 173 | } | |
| 174 | } | |
| 175 | ||
| 176 | 163 | // The URL passed the check when it was saved; the answer at sync time |
| 177 | 164 | // is what counts. |
| 178 | 165 | func TestSyncRefusesAPrivateAddressAtSyncTime(t *testing.T) { |
| @@ -238,22 +225,3 @@ func TestSyncRefusesANonHTTPScheme(t *testing.T) { | ||
| 238 | 225 | t.Fatalf("sync = %v, want a refusal", err) |
| 239 | 226 | } |
| 240 | 227 | } |
| 241 | ||
| 242 | func TestPinArgs(t *testing.T) { | |
| 243 | u, _ := url.Parse("https://git.example/x.git") | |
| 244 | got := pinArgs(u, []net.IP{net.ParseIP("203.0.113.5"), net.ParseIP("2001:db8::1")}) | |
| 245 | want := []string{"-c", "http.followRedirects=false", | |
| 246 | "-c", "http.curloptResolve=git.example:443:203.0.113.5,[2001:db8::1]"} | |
| 247 | if !slices.Equal(got, want) { | |
| 248 | t.Fatalf("https: %q", got) | |
| 249 | } | |
| 250 | u, _ = url.Parse("http://git.example:8080/x.git") | |
| 251 | if got := pinArgs(u, []net.IP{net.ParseIP("203.0.113.5")}); got[3] != "http.curloptResolve=git.example:8080:203.0.113.5" { | |
| 252 | t.Fatalf("http with port: %q", got) | |
| 253 | } | |
| 254 | // An address literal is its own resolution; there is nothing to pin. | |
| 255 | u, _ = url.Parse("https://203.0.113.5/x.git") | |
| 256 | if got := pinArgs(u, []net.IP{net.ParseIP("203.0.113.5")}); !slices.Equal(got, []string{"-c", "http.followRedirects=false"}) { | |
| 257 | t.Fatalf("literal: %q", got) | |
| 258 | } | |
| 259 | } | |