blob: c921278e87bcd7c9acbb560d87c78300a3243c1f [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"
10 "os/exec"
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070011 "runtime"
Earl Lee2e463fb2025-04-17 11:22:22 -070012 "strings"
Philip Zeyliger51e8e2b2025-05-09 21:41:12 +000013 "time"
Earl Lee2e463fb2025-04-17 11:22:22 -070014)
15
16type gitHTTP struct {
17 gitRepoRoot string
Josh Bleecher Snyder9f6a9982025-04-22 17:34:15 -070018 pass []byte
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070019 browserC chan bool // browser launch requests
Earl Lee2e463fb2025-04-17 11:22:22 -070020}
21
22func (g *gitHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
23 defer func() {
24 if err := recover(); err != nil {
25 slog.ErrorContext(r.Context(), "gitHTTP.ServeHTTP panic", slog.Any("recovered_err", err))
26
27 // Return an error response to the client
28 http.Error(w, fmt.Sprintf("panic: %v\n", err), http.StatusInternalServerError)
29 }
30 }()
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070031
32 // Get the Authorization header
33 username, password, ok := r.BasicAuth()
34
35 // Check if credentials were provided
36 if !ok {
37 // No credentials provided, return 401 Unauthorized
38 w.Header().Set("WWW-Authenticate", `Basic realm="Sketch Git Repository"`)
39 http.Error(w, "Unauthorized", http.StatusUnauthorized)
40 slog.InfoContext(r.Context(), "githttp: denied (basic auth)", "remote addr", r.RemoteAddr)
Earl Lee2e463fb2025-04-17 11:22:22 -070041 return
42 }
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070043
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070044 // Check if credentials are valid
Josh Bleecher Snyder9f6a9982025-04-22 17:34:15 -070045 if username != "sketch" || subtle.ConstantTimeCompare([]byte(password), g.pass) != 1 {
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070046 w.Header().Set("WWW-Authenticate", `Basic realm="Git Repository"`)
47 http.Error(w, "Unauthorized", http.StatusUnauthorized)
48 slog.InfoContext(r.Context(), "githttp: denied (basic auth)", "remote addr", r.RemoteAddr)
49 return
50 }
51
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000052 // TODO: real mux?
53 if strings.HasPrefix(r.URL.Path, "/browser") {
54 if r.Method != http.MethodPost {
55 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
56 return
57 }
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000058 defer r.Body.Close()
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070059
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000060 select {
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070061 case g.browserC <- true:
62 slog.InfoContext(r.Context(), "open browser requested")
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000063 w.WriteHeader(http.StatusOK)
64 default:
65 http.Error(w, "Too many browser launch requests", http.StatusTooManyRequests)
66 }
67 return
68 }
69
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070070 if runtime.GOOS == "darwin" {
71 // On the Mac, Docker connections show up from localhost. On Linux, the docker
72 // network is more arbitrary, so we don't do this additional check there.
73 if !strings.HasPrefix(r.RemoteAddr, "127.0.0.1:") {
74 slog.InfoContext(r.Context(), "githttp: denied", "remote addr", r.RemoteAddr)
75 http.Error(w, "no", http.StatusUnauthorized)
76 return
77 }
78 }
Earl Lee2e463fb2025-04-17 11:22:22 -070079 gitBin, err := exec.LookPath("git")
80 if err != nil {
81 http.Error(w, "no git: "+err.Error(), http.StatusInternalServerError)
82 return
83 }
84
Philip Zeyliger51e8e2b2025-05-09 21:41:12 +000085 // Check if this is a .git/info/refs request (which happens during git fetch)
86 // Use `GIT_CURL_VERBOSE=1 git fetch sketch-host` to inspect what's going on under the covers for git.
87 if strings.Contains(r.URL.Path, ".git/info/refs") {
88 slog.InfoContext(r.Context(), "detected git info/refs request, running git fetch origin")
89
90 // Create a context with a 5 second timeout
91 ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
92 defer cancel()
93
94 // Run git fetch origin in the background
95 cmd := exec.CommandContext(ctx, gitBin, "fetch", "origin")
96 cmd.Dir = g.gitRepoRoot
97
98 // Execute the command
99 output, err := cmd.CombinedOutput()
100 if err != nil {
101 slog.WarnContext(r.Context(), "git fetch failed",
102 "error", err,
103 "output", string(output))
104 // We don't return here, continue with normal processing
105 } else {
106 slog.InfoContext(r.Context(), "git fetch completed successfully")
107 }
108 }
109
Earl Lee2e463fb2025-04-17 11:22:22 -0700110 w.Header().Set("Cache-Control", "no-cache")
111 h := &cgi.Handler{
112 Path: gitBin,
113 Args: []string{"http-backend"},
114 Dir: g.gitRepoRoot,
115 Env: []string{
116 "GIT_PROJECT_ROOT=" + g.gitRepoRoot,
117 "PATH_INFO=" + r.URL.Path,
118 "QUERY_STRING=" + r.URL.RawQuery,
119 "REQUEST_METHOD=" + r.Method,
120 "GIT_HTTP_EXPORT_ALL=true",
121 "GIT_HTTP_ALLOW_REPACK=true",
122 "GIT_HTTP_ALLOW_PUSH=true",
123 "GIT_HTTP_VERBOSE=1",
124 },
125 }
126 h.ServeHTTP(w, r)
127}