Skip to content

Commit 3e58fe3

Browse files
author
Asim
committed
Prototype
0 parents  commit 3e58fe3

File tree

2 files changed

+396
-0
lines changed

2 files changed

+396
-0
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Git-Http-Backend.Go - Git Smart-HTTP Server Handler
2+
======================================================
3+
4+
This is a Go based implementation of Grack (a Rack application), which aimed
5+
to replace the builtin git-http-backed CGI handler distributed with C Git.
6+
Grack was written to allow far more webservers to handle Git smart http
7+
requests. The aim of this project is to improve Git smart http performance by
8+
utilising the power of Go.
9+
10+
Dependencies
11+
========================
12+
* Go - http://golang.org
13+
* Git >= 1.7
14+
15+
Quick Start
16+
========================
17+
$ (edit git-http-backend.go to set GitBinPath and ProjectRoot)
18+
$ go run git-http-backend.go
19+
$ git clone http://127.0.0.1:8080/asim/git-http-backend.git
20+
21+
License
22+
========================
23+
(The MIT License)
24+
25+
Copyright (c) 2013 Asim Aslam <[email protected]>
26+
27+
Permission is hereby granted, free of charge, to any person obtaining
28+
a copy of this software and associated documentation files (the
29+
'Software'), to deal in the Software without restriction, including
30+
without limitation the rights to use, copy, modify, merge, publish,
31+
distribute, sublicense, and/or sell copies of the Software, and to
32+
permit persons to whom the Software is furnished to do so, subject to
33+
the following conditions:
34+
35+
The above copyright notice and this permission notice shall be
36+
included in all copies or substantial portions of the Software.
37+
38+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
39+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
40+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
41+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
42+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
43+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
44+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

git-http-backend.go

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"io/ioutil"
7+
"log"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"path"
12+
"regexp"
13+
"strings"
14+
"strconv"
15+
"time"
16+
)
17+
18+
type Service struct {
19+
Method string
20+
Handler func(HandlerReq)
21+
Rpc string
22+
}
23+
24+
type Config struct {
25+
ProjectRoot string
26+
GitBinPath string
27+
UploadPack bool
28+
ReceivePack bool
29+
}
30+
31+
type HandlerReq struct {
32+
w http.ResponseWriter
33+
r *http.Request
34+
Rpc string
35+
Dir string
36+
File string
37+
}
38+
39+
var config Config = Config{
40+
ProjectRoot: "/tmp",
41+
GitBinPath: "/usr/bin/git",
42+
UploadPack: true,
43+
ReceivePack: true,
44+
}
45+
46+
var services = map[string] Service {
47+
"(.*?)/git-upload-pack$": Service{"POST", service_rpc, "upload-pack"},
48+
"(.*?)/git-receive-pack$": Service{"POST", service_rpc, "receive-pack"},
49+
"(.*?)/info/refs$": Service{"GET", get_info_refs, ""},
50+
"(.*?)/HEAD$": Service{"GET", get_text_file, ""},
51+
"(.*?)/objects/info/alternates$": Service{"GET", get_text_file, ""},
52+
"(.*?)/objects/info/http-alternates$": Service{"GET", get_text_file, ""},
53+
"(.*?)/objects/info/packs$": Service{"GET", get_info_packs, ""},
54+
"(.*?)/objects/info/[^/]*$": Service{"GET", get_text_file, ""},
55+
"(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$": Service{"GET", get_loose_object, ""},
56+
"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$": Service{"GET", get_pack_file, ""},
57+
"(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$": Service{"GET", get_idx_file, ""},
58+
}
59+
60+
// Request handling function
61+
62+
func request_handler() http.HandlerFunc {
63+
return func(w http.ResponseWriter, r *http.Request) {
64+
log.Printf("%s %s %s %s", r.RemoteAddr, r.Method, r.URL.Path, r.Proto)
65+
for match, service := range services {
66+
re, err := regexp.Compile(match)
67+
if err != nil {
68+
log.Print(err)
69+
}
70+
71+
if m := re.FindStringSubmatch(r.URL.Path); m != nil {
72+
if service.Method != r.Method {
73+
render_method_not_allowed(w, r)
74+
return
75+
}
76+
77+
rpc := service.Rpc
78+
file := strings.Replace(r.URL.Path, m[1] + "/", "", 1)
79+
dir, err := get_git_dir(m[1])
80+
81+
if err != nil {
82+
log.Print(err)
83+
render_not_found(w)
84+
return
85+
}
86+
87+
hr := HandlerReq{w, r, rpc, dir, file}
88+
service.Handler(hr)
89+
return
90+
}
91+
}
92+
render_not_found(w)
93+
return
94+
}
95+
}
96+
97+
// Actual command handling functions
98+
99+
func service_rpc(hr HandlerReq) {
100+
w, r, rpc, dir := hr.w, hr.r, hr.Rpc, hr.Dir
101+
access := has_access(r, dir, rpc, true)
102+
103+
if access == false {
104+
render_no_access(w)
105+
return
106+
}
107+
108+
input, _ := ioutil.ReadAll(r.Body)
109+
110+
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc))
111+
w.WriteHeader(http.StatusOK)
112+
113+
args := []string{rpc, "--stateless-rpc", dir}
114+
cmd := exec.Command(config.GitBinPath, args...)
115+
cmd.Dir = dir
116+
in, err := cmd.StdinPipe()
117+
if err != nil {
118+
log.Print(err)
119+
}
120+
121+
stdout, err := cmd.StdoutPipe()
122+
if err != nil {
123+
log.Print(err)
124+
}
125+
126+
err = cmd.Start()
127+
if err != nil {
128+
log.Print(err)
129+
}
130+
131+
in.Write(input)
132+
io.Copy(w, stdout)
133+
cmd.Wait()
134+
}
135+
136+
func get_info_refs(hr HandlerReq) {
137+
w, r, dir := hr.w, hr.r, hr.Dir
138+
service_name := get_service_type(r)
139+
access := has_access(r, dir, service_name, false)
140+
141+
if access {
142+
args := []string{service_name, "--stateless-rpc", "--advertise-refs", "."}
143+
refs := git_command(dir, args...)
144+
145+
hdr_nocache(w)
146+
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service_name))
147+
w.WriteHeader(http.StatusOK)
148+
w.Write(packet_write("# service=git-" + service_name + "\n"))
149+
w.Write(packet_flush())
150+
w.Write(refs)
151+
} else {
152+
update_server_info(dir)
153+
hdr_nocache(w)
154+
send_file("text/plain; charset=utf-8", hr)
155+
}
156+
}
157+
158+
func get_info_packs(hr HandlerReq) {
159+
hdr_cache_forever(hr.w)
160+
send_file("text/plain; charset=utf-8", hr)
161+
}
162+
163+
func get_loose_object(hr HandlerReq) {
164+
hdr_cache_forever(hr.w)
165+
send_file("application/x-git-loose-object", hr)
166+
}
167+
168+
func get_pack_file(hr HandlerReq) {
169+
hdr_cache_forever(hr.w)
170+
send_file("application/x-git-packed-objects", hr)
171+
}
172+
173+
func get_idx_file(hr HandlerReq) {
174+
hdr_cache_forever(hr.w)
175+
send_file("application/x-git-packed-objects-toc", hr)
176+
}
177+
178+
func get_text_file(hr HandlerReq) {
179+
hdr_nocache(hr.w)
180+
send_file("text/plain", hr)
181+
}
182+
183+
// Logic helping functions
184+
185+
func send_file(content_type string, hr HandlerReq) {
186+
w, r := hr.w, hr.r
187+
req_file := path.Join(hr.Dir, hr.File)
188+
189+
f, err := os.Stat(req_file)
190+
if os.IsNotExist(err) {
191+
render_not_found(w)
192+
return
193+
}
194+
195+
w.Header().Set("Content-Type", content_type)
196+
w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size()))
197+
w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat))
198+
http.ServeFile(w, r, req_file)
199+
}
200+
201+
202+
func get_git_dir(file_path string) (string, error) {
203+
root := config.ProjectRoot
204+
205+
if root == "" {
206+
cwd, err := os.Getwd()
207+
208+
if err != nil {
209+
log.Print(err)
210+
return "", err
211+
}
212+
213+
root = cwd
214+
}
215+
216+
f := path.Join(root, file_path)
217+
if _, err := os.Stat(f); os.IsNotExist(err) {
218+
return "", err
219+
}
220+
221+
return f, nil
222+
}
223+
224+
func get_service_type(r *http.Request) string {
225+
service_type := r.FormValue("service")
226+
227+
if s := strings.HasPrefix(service_type, "git-"); !s {
228+
return ""
229+
}
230+
231+
return strings.Replace(service_type, "git-", "", 1)
232+
}
233+
234+
func has_access(r *http.Request, dir string, rpc string, check_content_type bool) bool {
235+
if check_content_type {
236+
if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) {
237+
return false
238+
}
239+
}
240+
241+
if ! (rpc == "upload-pack" || rpc == "receive-pack") {
242+
return false
243+
}
244+
if rpc == "receive-pack" {
245+
return config.ReceivePack
246+
}
247+
if rpc == "upload-pack" {
248+
return config.UploadPack
249+
}
250+
251+
return get_config_setting(rpc, dir)
252+
}
253+
254+
func get_config_setting(service_name string, dir string) bool {
255+
service_name = strings.Replace(service_name, "-", "", -1)
256+
setting := get_git_config("http." + service_name, dir)
257+
258+
if service_name == "uploadpack" {
259+
return setting != "false"
260+
}
261+
262+
return setting == "true"
263+
}
264+
265+
func get_git_config(config_name string, dir string) string {
266+
args := []string{"config", config_name}
267+
out := string(git_command(dir, args...))
268+
return out[0:len(out)-1]
269+
}
270+
271+
func update_server_info(dir string) []byte {
272+
args := []string{"update-server-info"}
273+
return git_command(dir, args...)
274+
}
275+
276+
func git_command(dir string, args ...string) []byte {
277+
command := exec.Command(config.GitBinPath, args...)
278+
command.Dir = dir
279+
out, err := command.Output()
280+
281+
if err != nil {
282+
log.Print(err)
283+
}
284+
285+
return out
286+
}
287+
288+
// HTTP error response handling functions
289+
290+
func render_method_not_allowed(w http.ResponseWriter, r *http.Request) {
291+
if r.Proto == "HTTP/1.1" {
292+
w.WriteHeader(http.StatusMethodNotAllowed)
293+
w.Write([]byte("Method Not Allowed"))
294+
} else {
295+
w.WriteHeader(http.StatusBadRequest)
296+
w.Write([]byte("Bad Request"))
297+
}
298+
}
299+
300+
func render_not_found(w http.ResponseWriter) {
301+
w.WriteHeader(http.StatusNotFound)
302+
w.Write([]byte("Not Found"))
303+
}
304+
305+
func render_no_access(w http.ResponseWriter) {
306+
w.WriteHeader(http.StatusForbidden)
307+
w.Write([]byte("Forbidden"))
308+
}
309+
310+
// Packet-line handling function
311+
312+
func packet_flush() []byte {
313+
return []byte("0000")
314+
}
315+
316+
func packet_write(str string) []byte {
317+
s := strconv.FormatInt(int64(len(str) + 4), 16)
318+
319+
if len(s) % 4 != 0 {
320+
s = strings.Repeat("0", 4 - len(s) % 4) + s
321+
}
322+
323+
return []byte(s + str)
324+
}
325+
326+
// Header writing functions
327+
328+
func hdr_nocache(w http.ResponseWriter) {
329+
w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
330+
w.Header().Set("Pragma", "no-cache")
331+
w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
332+
}
333+
334+
func hdr_cache_forever(w http.ResponseWriter) {
335+
now := time.Now().Unix()
336+
expires := now + 31536000
337+
w.Header().Set("Date", fmt.Sprintf("%d", now))
338+
w.Header().Set("Expires", fmt.Sprintf("%d", expires))
339+
w.Header().Set("Cache-Control", "public, max-age=31536000")
340+
}
341+
342+
// Main
343+
344+
func main() {
345+
http.HandleFunc("/", request_handler())
346+
347+
err := http.ListenAndServe(":8080", nil)
348+
if err != nil {
349+
log.Fatal("ListenAndServe: ", err)
350+
}
351+
}
352+

0 commit comments

Comments
 (0)