From d0cb5e2318d1859f2dc9027151b4e4f1c973c6a1 Mon Sep 17 00:00:00 2001 From: Max Resnick Date: Sat, 29 Oct 2022 08:41:15 -0700 Subject: initial commit --- README.md | 19 ++++++++++ go.mod | 10 +++++ go.sum | 13 +++++++ main.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fbd96e --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# go-git-server + +`go-git-server` is an experimental web server that provides authentication and authorization for git repositories. + +## Design + +Initially `go-git-server` wraps the built-in git-http-backend CGI process. This is done to provide complete compatibility out of the box. In the future a native go backend could be created but there's no compelling story to re-write the backend. + +Authentication is done using a token that is generated by the server and is a fixed length 28 with the full 255 character range vs the normal ASCII range. The secret is then base64 encoded. Potentially in the future an OAuth token or client side TLS could be implemented. + +Authorization is implemented using [casbin.](https://github.com/casbin/casbin) Casbin allows for a flexible authorization models that can potentially provide some extensive controls. + +## Focus + +The current focus is for a single user and CI user(s) and intends to become self hosted as soon as possible. The focus is to simplify ongoing maintance and hosting simplicity. It's specifically designed for running in kubernetes. + +## Why + +Tools like gitea are great, but they require things like a DBMS. This increases hosting comlexity and maintenance especially for small teams or single user bases. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0ceffaf --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.ofmax.li/go-get-server + +go 1.19 + +require golang.org/x/crypto v0.0.0-20221012134737-56aed061732a + +require ( + github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect + github.com/casbin/casbin/v2 v2.56.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3dcf68b --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/casbin/casbin/v2 v2.56.0 h1:4qM+hDfj+i9M6lBbguafWKE/8tJA+9vRY5+l0ZB5WTo= +github.com/casbin/casbin/v2 v2.56.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= +golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= diff --git a/main.go b/main.go new file mode 100644 index 0000000..0db6ee1 --- /dev/null +++ b/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/csv" + "flag" + "fmt" + "log" + "math/big" + "net/http" + "net/http/cgi" + "os" + + "golang.org/x/crypto/bcrypt" +) + +var ( + reposDir = flag.String("r", ".", "Directory containing git repositories") + backendCommand = flag.String("c", "git http-backend", "CGI binary to execute") + addr = flag.String("l", ":8080", "Address/port to listen on") + newToken = flag.Bool("t", false, "make a new token") + authMap = map[string]string{} +) + +func LoadTokens() (map[string]string, error) { + tokenMap := make(map[string]string) + contents, err := os.Open("tokens.csv") + if err != nil { + fmt.Println("File reading error", err) + return tokenMap, err + } + defer contents.Close() + r := csv.NewReader(contents) + tokens, err := r.ReadAll() + if err != nil { + fmt.Println("File reading error", err) + return tokenMap, err + } + for _, acctToken := range tokens { + acct, hash := acctToken[0], acctToken[1] + tokenMap[acct] = hash + } + return tokenMap, err +} + +func NewToken() (string, string, error) { + tokenBytes := make([]byte, 28) + for i := range tokenBytes { + max := big.NewInt(int64(255)) + randInt, err := rand.Int(rand.Reader, max) + if err != nil { + return "", "", err + } + tokenBytes[i] = uint8(randInt.Int64()) + } + hashBytes, err := bcrypt.GenerateFromPassword(tokenBytes, bcrypt.DefaultCost) + if err != nil { + return "", "", err + } + token := base64.URLEncoding.EncodeToString(tokenBytes) + hash := string(hashBytes) + return token, hash, nil +} + +type Handler struct { + cgiHandler *cgi.Handler +} + +func NewHandler(reposDir, backendCommand string) *Handler { + return &Handler{ + &cgi.Handler{ + Path: "/bin/sh", + Args: []string{"-c", backendCommand}, + Dir: ".", + Env: []string{ + fmt.Sprintf("GIT_PROJECT_ROOT=%v", reposDir), + "GIT_HTTP_EXPORT_ALL=1", + }, + }, + } +} + +func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + u, p, ok := req.BasicAuth() + if !ok { + rw.Header().Set("WWW-Authenticate", `Basic realm="git"`) + http.Error(rw, "Authentication Required", 401) + return + } + hash, ok := authMap[fmt.Sprintf("user:%s", u)] + if !ok { + http.Error(rw, "Bad Request", 400) + return + } + token, err := base64.URLEncoding.DecodeString(p) + if err != nil { + http.Error(rw, "Bad Request", 400) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(hash), token); err != nil { + http.Error(rw, "Bad Request", 400) + return + } + h.cgiHandler.ServeHTTP(rw, req) +} + +func main() { + flag.Parse() + if *newToken { + token, hash, err := NewToken() + if err != nil { + log.Fatal(err) + } + fmt.Printf("token: %s\nhash: %s\n", token, hash) + return + } + tokens, err := LoadTokens() + fmt.Println(tokens) + if err != nil { + log.Fatal(err) + } + authMap = tokens + http.Handle("/", NewHandler(*reposDir, *backendCommand)) + log.Fatal(http.ListenAndServe(":8080", nil)) +} -- cgit v1.2.3