blob: ecd46a10895680169c25817f3ddbf579a7321b83 [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package dockerimg
2
3import (
Philip Zeyliger51e8e2b2025-05-09 21:41:12 +00004 "context"
Philip Zeyliger5e227dd2025-04-21 15:55:29 -07005 "crypto/subtle"
Earl Lee2e463fb2025-04-17 11:22:22 -07006 "fmt"
7 "log/slog"
8 "net/http"
9 "net/http/cgi"
Josh Bleecher Snyder784d5bd2025-07-11 00:09:30 +000010 "os"
Earl Lee2e463fb2025-04-17 11:22:22 -070011 "os/exec"
Josh Bleecher Snyder784d5bd2025-07-11 00:09:30 +000012 "path/filepath"
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070013 "runtime"
Earl Lee2e463fb2025-04-17 11:22:22 -070014 "strings"
Philip Zeyliger51e8e2b2025-05-09 21:41:12 +000015 "time"
Earl Lee2e463fb2025-04-17 11:22:22 -070016)
17
18type gitHTTP struct {
19 gitRepoRoot string
Josh Bleecher Snyder9f6a9982025-04-22 17:34:15 -070020 pass []byte
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070021 browserC chan bool // browser launch requests
Earl Lee2e463fb2025-04-17 11:22:22 -070022}
23
24func (g *gitHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25 defer func() {
26 if err := recover(); err != nil {
27 slog.ErrorContext(r.Context(), "gitHTTP.ServeHTTP panic", slog.Any("recovered_err", err))
28
29 // Return an error response to the client
30 http.Error(w, fmt.Sprintf("panic: %v\n", err), http.StatusInternalServerError)
31 }
32 }()
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070033
34 // Get the Authorization header
35 username, password, ok := r.BasicAuth()
36
37 // Check if credentials were provided
38 if !ok {
39 // No credentials provided, return 401 Unauthorized
40 w.Header().Set("WWW-Authenticate", `Basic realm="Sketch Git Repository"`)
41 http.Error(w, "Unauthorized", http.StatusUnauthorized)
42 slog.InfoContext(r.Context(), "githttp: denied (basic auth)", "remote addr", r.RemoteAddr)
Earl Lee2e463fb2025-04-17 11:22:22 -070043 return
44 }
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070045
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070046 // Check if credentials are valid
Josh Bleecher Snyder9f6a9982025-04-22 17:34:15 -070047 if username != "sketch" || subtle.ConstantTimeCompare([]byte(password), g.pass) != 1 {
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070048 w.Header().Set("WWW-Authenticate", `Basic realm="Git Repository"`)
49 http.Error(w, "Unauthorized", http.StatusUnauthorized)
50 slog.InfoContext(r.Context(), "githttp: denied (basic auth)", "remote addr", r.RemoteAddr)
51 return
52 }
53
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000054 // TODO: real mux?
55 if strings.HasPrefix(r.URL.Path, "/browser") {
56 if r.Method != http.MethodPost {
57 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
58 return
59 }
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000060 defer r.Body.Close()
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070061
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000062 select {
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070063 case g.browserC <- true:
64 slog.InfoContext(r.Context(), "open browser requested")
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000065 w.WriteHeader(http.StatusOK)
66 default:
67 http.Error(w, "Too many browser launch requests", http.StatusTooManyRequests)
68 }
69 return
70 }
71
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070072 if runtime.GOOS == "darwin" {
73 // On the Mac, Docker connections show up from localhost. On Linux, the docker
74 // network is more arbitrary, so we don't do this additional check there.
75 if !strings.HasPrefix(r.RemoteAddr, "127.0.0.1:") {
76 slog.InfoContext(r.Context(), "githttp: denied", "remote addr", r.RemoteAddr)
77 http.Error(w, "no", http.StatusUnauthorized)
78 return
79 }
80 }
Earl Lee2e463fb2025-04-17 11:22:22 -070081 gitBin, err := exec.LookPath("git")
82 if err != nil {
83 http.Error(w, "no git: "+err.Error(), http.StatusInternalServerError)
84 return
85 }
86
Philip Zeyligerc898abf2025-06-04 16:33:57 +000087 // Check if this is a .git/info/refs request for git-upload-pack service (which happens during git fetch)
Philip Zeyliger222bf412025-06-04 16:42:58 +000088 // Use `GIT_CURL_VERBOSE=1 git fetch origin` to inspect what's going on under the covers for git.
Philip Zeyligerc898abf2025-06-04 16:33:57 +000089 if r.Method == http.MethodGet && strings.Contains(r.URL.Path, ".git/info/refs") && r.URL.Query().Get("service") == "git-upload-pack" {
Philip Zeyliger51e8e2b2025-05-09 21:41:12 +000090 slog.InfoContext(r.Context(), "detected git info/refs request, running git fetch origin")
91
92 // Create a context with a 5 second timeout
93 ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
94 defer cancel()
95
96 // Run git fetch origin in the background
97 cmd := exec.CommandContext(ctx, gitBin, "fetch", "origin")
98 cmd.Dir = g.gitRepoRoot
99
100 // Execute the command
101 output, err := cmd.CombinedOutput()
102 if err != nil {
103 slog.WarnContext(r.Context(), "git fetch failed",
104 "error", err,
105 "output", string(output))
106 // We don't return here, continue with normal processing
107 } else {
108 slog.InfoContext(r.Context(), "git fetch completed successfully")
109 }
110 }
111
Josh Bleecher Snyder784d5bd2025-07-11 00:09:30 +0000112 // Dumb hack for bare repos: if the path starts with .git, and there is no .git, strip it off.
113 path := r.URL.Path
114 if _, err := os.Stat(filepath.Join(g.gitRepoRoot, path)); os.IsNotExist(err) {
115 path = strings.TrimPrefix(path, "/.git") // turn /.git/info/refs into /info/refs
116 }
117
Earl Lee2e463fb2025-04-17 11:22:22 -0700118 w.Header().Set("Cache-Control", "no-cache")
119 h := &cgi.Handler{
120 Path: gitBin,
121 Args: []string{"http-backend"},
122 Dir: g.gitRepoRoot,
123 Env: []string{
124 "GIT_PROJECT_ROOT=" + g.gitRepoRoot,
Josh Bleecher Snyder784d5bd2025-07-11 00:09:30 +0000125 "PATH_INFO=" + path,
Earl Lee2e463fb2025-04-17 11:22:22 -0700126 "QUERY_STRING=" + r.URL.RawQuery,
127 "REQUEST_METHOD=" + r.Method,
128 "GIT_HTTP_EXPORT_ALL=true",
129 "GIT_HTTP_ALLOW_REPACK=true",
130 "GIT_HTTP_ALLOW_PUSH=true",
131 "GIT_HTTP_VERBOSE=1",
132 },
133 }
134 h.ServeHTTP(w, r)
135}