aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md19
-rw-r--r--go.mod10
-rw-r--r--go.sum13
-rw-r--r--main.go126
4 files changed, 168 insertions, 0 deletions
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))
+}