Skip to content

Instantly share code, notes, and snippets.

@shanzi
Last active February 26, 2024 03:42
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save shanzi/1aa571f8f3b8f4608d60 to your computer and use it in GitHub Desktop.
Save shanzi/1aa571f8f3b8f4608d60 to your computer and use it in GitHub Desktop.
A simple Git Http Server in go
/*
gittip: a basic git http server.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright 2014 Chase Zhang <yun.er.run@gmail.com>
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