package githttp import ( "fmt" "io" "net/http" "os" "os/exec" "path" "strings" ) type GitHttp struct { // Root directory to serve repos from ProjectRoot string // Path to git binary GitBinPath string // Access rules UploadPack bool ReceivePack bool // Event handling functions EventHandler func(ev Event) } // Implement the http.Handler interface func (g *GitHttp) ServeHTTP(w http.ResponseWriter, r *http.Request) { g.requestHandler(w, r) return } // Shorthand constructor for most common scenario func New(root string) *GitHttp { return &GitHttp{ ProjectRoot: root, GitBinPath: "/usr/bin/git", UploadPack: true, ReceivePack: true, } } // Build root directory if doesn't exist func (g *GitHttp) Init() (*GitHttp, error) { if err := os.MkdirAll(g.ProjectRoot, os.ModePerm); err != nil { return nil, err } return g, nil } // Publish event if EventHandler is set func (g *GitHttp) event(e Event) { if g.EventHandler != nil { g.EventHandler(e) } else { fmt.Printf("EVENT: %q\n", e) } } // Actual command handling functions func (g *GitHttp) serviceRpc(hr HandlerReq) error { w, r, rpc, dir := hr.w, hr.r, hr.Rpc, hr.Dir access, err := g.hasAccess(r, dir, rpc, true) if err != nil { return err } if access == false { return &ErrorNoAccess{hr.Dir} } // Reader that decompresses if necessary reader, err := requestReader(r) if err != nil { return err } defer reader.Close() // Reader that scans for events rpcReader := &RpcReader{ Reader: reader, Rpc: rpc, } // Set content type w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc)) args := []string{rpc, "--stateless-rpc", "."} cmd := exec.Command(g.GitBinPath, args...) cmd.Dir = dir stdin, err := cmd.StdinPipe() if err != nil { return err } stdout, err := cmd.StdoutPipe() if err != nil { return err } defer stdout.Close() err = cmd.Start() if err != nil { return err } // Scan's git command's output for errors gitReader := &GitReader{ Reader: stdout, } // Copy input to git binary io.Copy(stdin, rpcReader) stdin.Close() // Write git binary's output to http response io.Copy(w, gitReader) // Wait till command has completed mainError := cmd.Wait() if mainError == nil { mainError = gitReader.GitError } // Fire events for _, e := range rpcReader.Events { // Set directory to current repo e.Dir = dir e.Request = hr.r e.Error = mainError // Fire event g.event(e) } // Because a response was already written, // the header cannot be changed return nil } func (g *GitHttp) getInfoRefs(hr HandlerReq) error { w, r, dir := hr.w, hr.r, hr.Dir service_name := getServiceType(r) access, err := g.hasAccess(r, dir, service_name, false) if err != nil { return err } if !access { g.updateServerInfo(dir) hdrNocache(w) return sendFile("text/plain; charset=utf-8", hr) } args := []string{service_name, "--stateless-rpc", "--advertise-refs", "."} refs, err := g.gitCommand(dir, args...) if err != nil { return err } hdrNocache(w) w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service_name)) w.WriteHeader(http.StatusOK) w.Write(packetWrite("# service=git-" + service_name + "\n")) w.Write(packetFlush()) w.Write(refs) return nil } func (g *GitHttp) getInfoPacks(hr HandlerReq) error { hdrCacheForever(hr.w) return sendFile("text/plain; charset=utf-8", hr) } func (g *GitHttp) getLooseObject(hr HandlerReq) error { hdrCacheForever(hr.w) return sendFile("application/x-git-loose-object", hr) } func (g *GitHttp) getPackFile(hr HandlerReq) error { hdrCacheForever(hr.w) return sendFile("application/x-git-packed-objects", hr) } func (g *GitHttp) getIdxFile(hr HandlerReq) error { hdrCacheForever(hr.w) return sendFile("application/x-git-packed-objects-toc", hr) } func (g *GitHttp) getTextFile(hr HandlerReq) error { hdrNocache(hr.w) return sendFile("text/plain", hr) } // Logic helping functions func sendFile(content_type string, hr HandlerReq) error { w, r := hr.w, hr.r req_file := path.Join(hr.Dir, hr.File) f, err := os.Stat(req_file) if err != nil { return err } w.Header().Set("Content-Type", content_type) w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size())) w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat)) http.ServeFile(w, r, req_file) return nil } func (g *GitHttp) getGitDir(file_path string) (string, error) { root := g.ProjectRoot if root == "" { cwd, err := os.Getwd() if err != nil { return "", err } root = cwd } f := path.Join(root, file_path) if _, err := os.Stat(f); os.IsNotExist(err) { return "", err } return f, nil } func (g *GitHttp) hasAccess(r *http.Request, dir string, rpc string, check_content_type bool) (bool, error) { if check_content_type { if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) { return false, nil } } if !(rpc == "upload-pack" || rpc == "receive-pack") { return false, nil } if rpc == "receive-pack" { return g.ReceivePack, nil } if rpc == "upload-pack" { return g.UploadPack, nil } return g.getConfigSetting(rpc, dir) } func (g *GitHttp) getConfigSetting(service_name string, dir string) (bool, error) { service_name = strings.Replace(service_name, "-", "", -1) setting, err := g.getGitConfig("http."+service_name, dir) if err != nil { return false, nil } if service_name == "uploadpack" { return setting != "false", nil } return setting == "true", nil } func (g *GitHttp) getGitConfig(config_name string, dir string) (string, error) { args := []string{"config", config_name} out, err := g.gitCommand(dir, args...) if err != nil { return "", err } return string(out)[0 : len(out)-1], nil } func (g *GitHttp) updateServerInfo(dir string) ([]byte, error) { args := []string{"update-server-info"} return g.gitCommand(dir, args...) } func (g *GitHttp) gitCommand(dir string, args ...string) ([]byte, error) { command := exec.Command(g.GitBinPath, args...) command.Dir = dir return command.Output() }