package main import ( "compress/gzip" "crypto/sha256" "crypto/subtle" "fmt" "io" "log" "net/http" "os" "os/exec" ) // Smart HTTP endpoints. frieren speaks just enough of the transport to route // and authenticate, then hands the byte stream to git's own plumbing: // // GET /{repo}(.git)/info/refs?service=git-upload-pack → ref advertisement (anonymous) // POST /{repo}(.git)/git-upload-pack → clone/fetch (anonymous) // GET /{repo}(.git)/info/refs?service=git-receive-pack → push advertisement (owner only) // POST /{repo}(.git)/git-receive-pack → push (owner only) // // A push to a repository that doesn't exist yet creates it (owner only). func pktLine(s string) string { return fmt.Sprintf("%04x%s", len(s)+4, s) } // authorized reports whether the request carries the owner token as the HTTP // Basic password. The username is ignored. With no token configured the // server is read-only for everyone, owner included. func (srv *Server) authorized(r *http.Request) bool { if srv.Token == "" { return false } _, pass, ok := r.BasicAuth() if !ok { return false } want := sha256.Sum256([]byte(srv.Token)) got := sha256.Sum256([]byte(pass)) return subtle.ConstantTimeCompare(want[:], got[:]) == 1 } func requireAuth(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", `Basic realm="frieren"`) http.Error(w, "authentication required", http.StatusUnauthorized) } // ensureRepo resolves the repo for a push, creating it on first push. func (srv *Server) ensureRepo(w http.ResponseWriter, name string) bool { if srv.Store.exists(name) { return true } if !validRepoName(name) { http.Error(w, "invalid repository name", http.StatusBadRequest) return false } if err := srv.Store.create(name, ""); err != nil { log.Printf("create %s: %v", name, err) http.Error(w, "could not create repository", http.StatusInternalServerError) return false } log.Printf("created repository %s.git", name) return true } func (srv *Server) infoRefs(w http.ResponseWriter, r *http.Request) { name := repoParam(r) service := r.URL.Query().Get("service") switch service { case "git-upload-pack": if !srv.Store.exists(name) { http.NotFound(w, r) return } case "git-receive-pack": if !srv.authorized(r) { requireAuth(w) return } if !srv.ensureRepo(w, name) { return } default: http.Error(w, "smart HTTP only", http.StatusForbidden) return } sub := service[len("git-"):] cmd := exec.CommandContext(r.Context(), "git", sub, "--stateless-rpc", "--advertise-refs", srv.Store.repoPath(name)) cmd.Env = gitEnv(r) out, err := cmd.Output() if err != nil { log.Printf("%s advertise %s: %v", sub, name, err) http.Error(w, "git error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-"+service+"-advertisement") w.Header().Set("Cache-Control", "no-cache") io.WriteString(w, pktLine("# service="+service+"\n")) io.WriteString(w, "0000") w.Write(out) } func (srv *Server) uploadPack(w http.ResponseWriter, r *http.Request) { name := repoParam(r) if !srv.Store.exists(name) { http.NotFound(w, r) return } srv.serviceRPC(w, r, name, "upload-pack") } func (srv *Server) receivePack(w http.ResponseWriter, r *http.Request) { name := repoParam(r) if !srv.authorized(r) { requireAuth(w) return } if !srv.ensureRepo(w, name) { return } srv.serviceRPC(w, r, name, "receive-pack") } func (srv *Server) serviceRPC(w http.ResponseWriter, r *http.Request, name, sub string) { body := io.Reader(r.Body) if r.Header.Get("Content-Encoding") == "gzip" { gz, err := gzip.NewReader(body) if err != nil { http.Error(w, "bad gzip body", http.StatusBadRequest) return } defer gz.Close() body = gz } cmd := exec.CommandContext(r.Context(), "git", sub, "--stateless-rpc", srv.Store.repoPath(name)) cmd.Env = gitEnv(r) cmd.Stdin = body cmd.Stderr = os.Stderr stdout, err := cmd.StdoutPipe() if err != nil { http.Error(w, "git error", http.StatusInternalServerError) return } if err := cmd.Start(); err != nil { log.Printf("%s %s: %v", sub, name, err) http.Error(w, "git error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-git-"+sub+"-result") w.Header().Set("Cache-Control", "no-cache") io.Copy(newFlushWriter(w), stdout) if err := cmd.Wait(); err != nil { // Headers are already sent; all we can do is log. log.Printf("%s %s: %v", sub, name, err) } } // gitEnv forwards the client's Git-Protocol header so protocol v2 works. func gitEnv(r *http.Request) []string { env := os.Environ() if p := r.Header.Get("Git-Protocol"); p != "" { env = append(env, "GIT_PROTOCOL="+p) } return env } type flushWriter struct { w io.Writer f http.Flusher } func newFlushWriter(w http.ResponseWriter) io.Writer { if f, ok := w.(http.Flusher); ok { return &flushWriter{w: w, f: f} } return w } func (fw *flushWriter) Write(p []byte) (int, error) { n, err := fw.w.Write(p) fw.f.Flush() return n, err }