Last active
February 26, 2024 03:42
-
-
Save shanzi/1aa571f8f3b8f4608d60 to your computer and use it in GitHub Desktop.
A simple Git Http Server in go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
gittip: a basic git http server. | |
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | |
Version 2, December 2004 | |
Copyright 2014 Chase Zhang <[email protected]> | |
Everyone is permitted to copy and distribute verbatim or modified | |
copies of this license document, and changing it is allowed as long | |
as the name is changed. | |
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | |
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | |
0. You just DO WHAT THE FUCK YOU WANT TO. | |
*/ | |
/* | |
Usage: | |
Make sure you have git installed and can be accessed by this program. | |
Put this file anywhere you like (typically under `$GOPATH/src/`) | |
and then install the only third-party dependency by: | |
go get github.com/zenazn/goji | |
After this, you start the git server by runing `go run gittp.go`. | |
You should change the variable `realm` which is used by Basic HTTP Auth | |
to identify your server and replace the user/password combinations | |
defined in `users`. | |
To create or delete repos: | |
curl -u [username]:[password] -X PUT http://localhost:8000/[repo name] | |
curl -u [username]:[password] -X DELETE http://localhost:8000/[repo name] | |
Please enjoy it! | |
*/ | |
package main | |
import ( | |
"encoding/base64" | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
"os/exec" | |
"path" | |
"regexp" | |
"strings" | |
"github.com/zenazn/goji" | |
"github.com/zenazn/goji/web" | |
) | |
const realm = "gittp" | |
// A map filled with allowed users and their passwords. | |
// Replace the contents with your own. | |
var users = map[string]string{ | |
"admin": "adminpassword", | |
} | |
var gitRoot = path.Join(os.TempDir(), "git_repo") | |
func createRepo(c web.C, w http.ResponseWriter, r *http.Request) { | |
reponame := c.URLParams["reponame"] | |
repopath := path.Join(gitRoot, reponame) | |
if _, err := os.Stat(repopath); err == nil { | |
w.WriteHeader(400) | |
fmt.Fprintf(w, "Repo `%s` already exists!\n", reponame) | |
} else { | |
gitInitCmd := exec.Command("git", "init", "--bare", repopath) | |
_, err := gitInitCmd.CombinedOutput() | |
if err != nil { | |
w.WriteHeader(500) | |
fmt.Fprintf(w, "Initialize git repo `%s` failed!\n", reponame) | |
} else { | |
fmt.Fprintf(w, "Empty git repo `%s` initialized!\n", reponame) | |
} | |
} | |
} | |
func deleteRepo(c web.C, w http.ResponseWriter, r *http.Request) { | |
reponame := c.URLParams["reponame"] | |
repopath := path.Join(gitRoot, reponame) | |
if _, err := os.Stat(repopath); os.IsNotExist(err) { | |
w.WriteHeader(400) | |
fmt.Fprintf(w, "Repo `%s` does not exist!\n", reponame) | |
} else { | |
err := os.RemoveAll(repopath) | |
if err != nil { | |
w.WriteHeader(500) | |
fmt.Fprintf(w, "Delete repo `%s` failed!\n", reponame) | |
} else { | |
fmt.Fprintf(w, "Repo `%s` deleted!\n", reponame) | |
} | |
} | |
} | |
func inforefs(c web.C, w http.ResponseWriter, r *http.Request) { | |
reponame := c.URLParams["reponame"] | |
repopath := path.Join(gitRoot, reponame) | |
service := r.FormValue("service") | |
if len(service) > 0 { | |
w.Header().Add("Content-type", fmt.Sprintf("application/x-%s-advertisement", service)) | |
gitLocalCmd := exec.Command( | |
"git", | |
string(service[4:]), | |
"--stateless-rpc", | |
"--advertise-refs", | |
repopath) | |
out, err := gitLocalCmd.CombinedOutput() | |
if err != nil { | |
w.WriteHeader(500) | |
fmt.Fprintln(w, "Internal Server Error") | |
w.Write(out) | |
} else { | |
serverAdvert := fmt.Sprintf("# service=%s", service) | |
length := len(serverAdvert) + 4 | |
fmt.Fprintf(w, "%04x%s0000", length, serverAdvert) | |
w.Write(out) | |
} | |
} else { | |
fmt.Fprintln(w, "Invalid request") | |
w.WriteHeader(400) | |
} | |
} | |
func rpc(c web.C, w http.ResponseWriter, r *http.Request) { | |
reponame := c.URLParams["reponame"] | |
repopath := path.Join(gitRoot, reponame) | |
command := c.URLParams["command"] | |
if len(command) > 0 { | |
w.Header().Add("Content-type", fmt.Sprintf("application/x-git-%s-result", command)) | |
w.WriteHeader(200) | |
gitCmd := exec.Command("git", command, "--stateless-rpc", repopath) | |
cmdIn, _ := gitCmd.StdinPipe() | |
cmdOut, _ := gitCmd.StdoutPipe() | |
body := r.Body | |
gitCmd.Start() | |
io.Copy(cmdIn, body) | |
io.Copy(w, cmdOut) | |
if command == "receive-pack" { | |
updateCmd := exec.Command("git", "--git-dir", repopath, "update-server-info") | |
updateCmd.Start() | |
} | |
} else { | |
w.WriteHeader(400) | |
fmt.Fprintln(w, "Invalid Request") | |
} | |
} | |
func generic(c web.C, w http.ResponseWriter, r *http.Request) { | |
reponame := c.URLParams["reponame"] | |
repopath := path.Join(gitRoot, reponame) | |
filepath := path.Join(gitRoot, r.URL.String()) | |
if strings.HasPrefix(filepath, repopath) { | |
http.ServeFile(w, r, filepath) | |
} else { | |
w.WriteHeader(404) | |
} | |
} | |
func basicAuthMiddleware(c *web.C, h http.Handler) http.Handler { | |
fn := func(w http.ResponseWriter, r *http.Request) { | |
authField := r.Header["Authorization"] | |
if len(authField) > 0 { | |
auth := authField[0] | |
parts := strings.Split(auth, " ") | |
if len(parts) == 2 { | |
authType := parts[0] | |
combination := parts[1] | |
if authType == "Basic" { | |
s, e := base64.StdEncoding.DecodeString(combination) | |
str := string(s) | |
if e == nil { | |
parts := strings.SplitN(str, ":", 2) | |
if len(parts) == 2 && users[parts[0]] == parts[1] { | |
h.ServeHTTP(w, r) | |
return | |
} | |
} | |
} | |
} | |
} | |
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm)) | |
w.WriteHeader(401) | |
fmt.Fprintln(w, "Unauthorized") | |
} | |
return http.HandlerFunc(fn) | |
} | |
func main() { | |
// create and delete repo | |
goji.Put("/:reponame", createRepo) | |
goji.Delete("/:reponame", deleteRepo) | |
// get repo info/refs | |
goji.Get("/:reponame/info/refs", inforefs) | |
goji.Head("/:reponame/info/refs", inforefs) | |
// RPC request on repo | |
goji.Post(regexp.MustCompile("^/(?P<reponame>[^/]+)/git-(?P<command>[^/]+)$"), rpc) | |
// access file contents | |
goji.Get("/:reponame/*", generic) | |
goji.Head("/:reponame/*", generic) | |
// start serving | |
goji.Use(basicAuthMiddleware) | |
goji.Serve() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment