blob: 7ce5f405fa8b09d6d01f4ea5548e6bbe39a0f9e7 [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package dockerimg
2
3import (
Philip Zeyliger5e227dd2025-04-21 15:55:29 -07004 "crypto/subtle"
Earl Lee2e463fb2025-04-17 11:22:22 -07005 "fmt"
6 "log/slog"
7 "net/http"
8 "net/http/cgi"
9 "os/exec"
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070010 "runtime"
Earl Lee2e463fb2025-04-17 11:22:22 -070011 "strings"
12)
13
14type gitHTTP struct {
15 gitRepoRoot string
Josh Bleecher Snyder9f6a9982025-04-22 17:34:15 -070016 pass []byte
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070017 browserC chan bool // browser launch requests
Earl Lee2e463fb2025-04-17 11:22:22 -070018}
19
20func (g *gitHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
21 defer func() {
22 if err := recover(); err != nil {
23 slog.ErrorContext(r.Context(), "gitHTTP.ServeHTTP panic", slog.Any("recovered_err", err))
24
25 // Return an error response to the client
26 http.Error(w, fmt.Sprintf("panic: %v\n", err), http.StatusInternalServerError)
27 }
28 }()
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070029
30 // Get the Authorization header
31 username, password, ok := r.BasicAuth()
32
33 // Check if credentials were provided
34 if !ok {
35 // No credentials provided, return 401 Unauthorized
36 w.Header().Set("WWW-Authenticate", `Basic realm="Sketch Git Repository"`)
37 http.Error(w, "Unauthorized", http.StatusUnauthorized)
38 slog.InfoContext(r.Context(), "githttp: denied (basic auth)", "remote addr", r.RemoteAddr)
Earl Lee2e463fb2025-04-17 11:22:22 -070039 return
40 }
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070041
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070042 // Check if credentials are valid
Josh Bleecher Snyder9f6a9982025-04-22 17:34:15 -070043 if username != "sketch" || subtle.ConstantTimeCompare([]byte(password), g.pass) != 1 {
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070044 w.Header().Set("WWW-Authenticate", `Basic realm="Git Repository"`)
45 http.Error(w, "Unauthorized", http.StatusUnauthorized)
46 slog.InfoContext(r.Context(), "githttp: denied (basic auth)", "remote addr", r.RemoteAddr)
47 return
48 }
49
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000050 // TODO: real mux?
51 if strings.HasPrefix(r.URL.Path, "/browser") {
52 if r.Method != http.MethodPost {
53 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
54 return
55 }
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000056 defer r.Body.Close()
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070057
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000058 select {
Josh Bleecher Snyder99570462025-05-05 10:26:14 -070059 case g.browserC <- true:
60 slog.InfoContext(r.Context(), "open browser requested")
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +000061 w.WriteHeader(http.StatusOK)
62 default:
63 http.Error(w, "Too many browser launch requests", http.StatusTooManyRequests)
64 }
65 return
66 }
67
Philip Zeyliger5e227dd2025-04-21 15:55:29 -070068 if runtime.GOOS == "darwin" {
69 // On the Mac, Docker connections show up from localhost. On Linux, the docker
70 // network is more arbitrary, so we don't do this additional check there.
71 if !strings.HasPrefix(r.RemoteAddr, "127.0.0.1:") {
72 slog.InfoContext(r.Context(), "githttp: denied", "remote addr", r.RemoteAddr)
73 http.Error(w, "no", http.StatusUnauthorized)
74 return
75 }
76 }
Earl Lee2e463fb2025-04-17 11:22:22 -070077 gitBin, err := exec.LookPath("git")
78 if err != nil {
79 http.Error(w, "no git: "+err.Error(), http.StatusInternalServerError)
80 return
81 }
82
83 w.Header().Set("Cache-Control", "no-cache")
84 h := &cgi.Handler{
85 Path: gitBin,
86 Args: []string{"http-backend"},
87 Dir: g.gitRepoRoot,
88 Env: []string{
89 "GIT_PROJECT_ROOT=" + g.gitRepoRoot,
90 "PATH_INFO=" + r.URL.Path,
91 "QUERY_STRING=" + r.URL.RawQuery,
92 "REQUEST_METHOD=" + r.Method,
93 "GIT_HTTP_EXPORT_ALL=true",
94 "GIT_HTTP_ALLOW_REPACK=true",
95 "GIT_HTTP_ALLOW_PUSH=true",
96 "GIT_HTTP_VERBOSE=1",
97 },
98 }
99 h.ServeHTTP(w, r)
100}