123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- 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()
- }
|