githttp.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. package githttp
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "os"
  7. "os/exec"
  8. "path"
  9. "strings"
  10. )
  11. type GitHttp struct {
  12. // Root directory to serve repos from
  13. ProjectRoot string
  14. // Path to git binary
  15. GitBinPath string
  16. // Access rules
  17. UploadPack bool
  18. ReceivePack bool
  19. // Event handling functions
  20. EventHandler func(ev Event)
  21. }
  22. // Implement the http.Handler interface
  23. func (g *GitHttp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  24. g.requestHandler(w, r)
  25. return
  26. }
  27. // Shorthand constructor for most common scenario
  28. func New(root string) *GitHttp {
  29. return &GitHttp{
  30. ProjectRoot: root,
  31. GitBinPath: "/usr/bin/git",
  32. UploadPack: true,
  33. ReceivePack: true,
  34. }
  35. }
  36. // Build root directory if doesn't exist
  37. func (g *GitHttp) Init() (*GitHttp, error) {
  38. if err := os.MkdirAll(g.ProjectRoot, os.ModePerm); err != nil {
  39. return nil, err
  40. }
  41. return g, nil
  42. }
  43. // Publish event if EventHandler is set
  44. func (g *GitHttp) event(e Event) {
  45. if g.EventHandler != nil {
  46. g.EventHandler(e)
  47. } else {
  48. fmt.Printf("EVENT: %q\n", e)
  49. }
  50. }
  51. // Actual command handling functions
  52. func (g *GitHttp) serviceRpc(hr HandlerReq) error {
  53. w, r, rpc, dir := hr.w, hr.r, hr.Rpc, hr.Dir
  54. access, err := g.hasAccess(r, dir, rpc, true)
  55. if err != nil {
  56. return err
  57. }
  58. if access == false {
  59. return &ErrorNoAccess{hr.Dir}
  60. }
  61. // Reader that decompresses if necessary
  62. reader, err := requestReader(r)
  63. if err != nil {
  64. return err
  65. }
  66. defer reader.Close()
  67. // Reader that scans for events
  68. rpcReader := &RpcReader{
  69. Reader: reader,
  70. Rpc: rpc,
  71. }
  72. // Set content type
  73. w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc))
  74. args := []string{rpc, "--stateless-rpc", "."}
  75. cmd := exec.Command(g.GitBinPath, args...)
  76. cmd.Dir = dir
  77. stdin, err := cmd.StdinPipe()
  78. if err != nil {
  79. return err
  80. }
  81. stdout, err := cmd.StdoutPipe()
  82. if err != nil {
  83. return err
  84. }
  85. defer stdout.Close()
  86. err = cmd.Start()
  87. if err != nil {
  88. return err
  89. }
  90. // Scan's git command's output for errors
  91. gitReader := &GitReader{
  92. Reader: stdout,
  93. }
  94. // Copy input to git binary
  95. io.Copy(stdin, rpcReader)
  96. stdin.Close()
  97. // Write git binary's output to http response
  98. io.Copy(w, gitReader)
  99. // Wait till command has completed
  100. mainError := cmd.Wait()
  101. if mainError == nil {
  102. mainError = gitReader.GitError
  103. }
  104. // Fire events
  105. for _, e := range rpcReader.Events {
  106. // Set directory to current repo
  107. e.Dir = dir
  108. e.Request = hr.r
  109. e.Error = mainError
  110. // Fire event
  111. g.event(e)
  112. }
  113. // Because a response was already written,
  114. // the header cannot be changed
  115. return nil
  116. }
  117. func (g *GitHttp) getInfoRefs(hr HandlerReq) error {
  118. w, r, dir := hr.w, hr.r, hr.Dir
  119. service_name := getServiceType(r)
  120. access, err := g.hasAccess(r, dir, service_name, false)
  121. if err != nil {
  122. return err
  123. }
  124. if !access {
  125. g.updateServerInfo(dir)
  126. hdrNocache(w)
  127. return sendFile("text/plain; charset=utf-8", hr)
  128. }
  129. args := []string{service_name, "--stateless-rpc", "--advertise-refs", "."}
  130. refs, err := g.gitCommand(dir, args...)
  131. if err != nil {
  132. return err
  133. }
  134. hdrNocache(w)
  135. w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service_name))
  136. w.WriteHeader(http.StatusOK)
  137. w.Write(packetWrite("# service=git-" + service_name + "\n"))
  138. w.Write(packetFlush())
  139. w.Write(refs)
  140. return nil
  141. }
  142. func (g *GitHttp) getInfoPacks(hr HandlerReq) error {
  143. hdrCacheForever(hr.w)
  144. return sendFile("text/plain; charset=utf-8", hr)
  145. }
  146. func (g *GitHttp) getLooseObject(hr HandlerReq) error {
  147. hdrCacheForever(hr.w)
  148. return sendFile("application/x-git-loose-object", hr)
  149. }
  150. func (g *GitHttp) getPackFile(hr HandlerReq) error {
  151. hdrCacheForever(hr.w)
  152. return sendFile("application/x-git-packed-objects", hr)
  153. }
  154. func (g *GitHttp) getIdxFile(hr HandlerReq) error {
  155. hdrCacheForever(hr.w)
  156. return sendFile("application/x-git-packed-objects-toc", hr)
  157. }
  158. func (g *GitHttp) getTextFile(hr HandlerReq) error {
  159. hdrNocache(hr.w)
  160. return sendFile("text/plain", hr)
  161. }
  162. // Logic helping functions
  163. func sendFile(content_type string, hr HandlerReq) error {
  164. w, r := hr.w, hr.r
  165. req_file := path.Join(hr.Dir, hr.File)
  166. f, err := os.Stat(req_file)
  167. if err != nil {
  168. return err
  169. }
  170. w.Header().Set("Content-Type", content_type)
  171. w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size()))
  172. w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat))
  173. http.ServeFile(w, r, req_file)
  174. return nil
  175. }
  176. func (g *GitHttp) getGitDir(file_path string) (string, error) {
  177. root := g.ProjectRoot
  178. if root == "" {
  179. cwd, err := os.Getwd()
  180. if err != nil {
  181. return "", err
  182. }
  183. root = cwd
  184. }
  185. f := path.Join(root, file_path)
  186. if _, err := os.Stat(f); os.IsNotExist(err) {
  187. return "", err
  188. }
  189. return f, nil
  190. }
  191. func (g *GitHttp) hasAccess(r *http.Request, dir string, rpc string, check_content_type bool) (bool, error) {
  192. if check_content_type {
  193. if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) {
  194. return false, nil
  195. }
  196. }
  197. if !(rpc == "upload-pack" || rpc == "receive-pack") {
  198. return false, nil
  199. }
  200. if rpc == "receive-pack" {
  201. return g.ReceivePack, nil
  202. }
  203. if rpc == "upload-pack" {
  204. return g.UploadPack, nil
  205. }
  206. return g.getConfigSetting(rpc, dir)
  207. }
  208. func (g *GitHttp) getConfigSetting(service_name string, dir string) (bool, error) {
  209. service_name = strings.Replace(service_name, "-", "", -1)
  210. setting, err := g.getGitConfig("http."+service_name, dir)
  211. if err != nil {
  212. return false, nil
  213. }
  214. if service_name == "uploadpack" {
  215. return setting != "false", nil
  216. }
  217. return setting == "true", nil
  218. }
  219. func (g *GitHttp) getGitConfig(config_name string, dir string) (string, error) {
  220. args := []string{"config", config_name}
  221. out, err := g.gitCommand(dir, args...)
  222. if err != nil {
  223. return "", err
  224. }
  225. return string(out)[0 : len(out)-1], nil
  226. }
  227. func (g *GitHttp) updateServerInfo(dir string) ([]byte, error) {
  228. args := []string{"update-server-info"}
  229. return g.gitCommand(dir, args...)
  230. }
  231. func (g *GitHttp) gitCommand(dir string, args ...string) ([]byte, error) {
  232. command := exec.Command(g.GitBinPath, args...)
  233. command.Dir = dir
  234. return command.Output()
  235. }