package main import ( "context" "crypto/rand" "encoding/base64" "encoding/csv" "flag" "fmt" "log" "math/big" "net/http" "net/http/cgi" "os" "github.com/casbin/casbin/v2" "golang.org/x/crypto/bcrypt" ) var ( reposDir = flag.String("r", "./repos", "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") ) // LoadTokens load tokens from a csv into a map func LoadTokens() (map[string]string, error) { tokenMap := make(map[string]string) // TODO this should be configurable 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 } // NewToken generate a new token 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 } // GitHttpBackendHandler a handler for git cgi func GitHttpBackendHandler(reposDir, backendCommand string) http.Handler { projectDirEnv := fmt.Sprintf("GIT_PROJECT_ROOT=%v", reposDir) return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() uid := ctx.Value("urn") git := &cgi.Handler{ Path: "/bin/sh", Args: []string{"-c", backendCommand}, Dir: ".", Env: []string{ projectDirEnv, "GIT_HTTP_EXPORT_ALL=1", fmt.Sprintf("REMOTE_USER=%s", uid), }, } git.ServeHTTP(rw, req) }) } // Authentication middleware to enforce authentication of all requests. func Authentication(authMap map[string]string, next http.Handler) http.Handler { return http.HandlerFunc(func(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 } urn := fmt.Sprintf("uid:%s", u) hash, ok := authMap[urn] 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 } ctx := context.WithValue(req.Context(), "urn", urn) next.ServeHTTP(rw, req.WithContext(ctx)) }) } func Authorization(enf *casbin.Enforcer, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() urn := ctx.Value("urn") repo := req.URL.Path action := req.Method // defer req.Body.Close() // body, _ := io.ReadAll(req.Body) // log.Printf("%s", body) ok, err := enf.Enforce(urn, repo, action) if err != nil { log.Printf("error running enforce %s", err) http.Error(rw, "Bad Request", http.StatusBadRequest) } if !ok { log.Printf("Access denied") http.Error(rw, "Access denied", http.StatusUnauthorized) } log.Printf("Method %s Url %s", action, repo) next.ServeHTTP(rw, req.WithContext(ctx)) }) } func main() { // TODO this should be configurable enf, _ := casbin.NewEnforcer("./auth_model.ini", "./policy.csv") 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() if err != nil { log.Fatal(err) } router := http.NewServeMux() // TODO we don't want to use a global // de-reference args router.Handle("/", GitHttpBackendHandler(*reposDir, *backendCommand)) mux := Authentication(tokens, Authorization(enf, router)) log.Fatal(http.ListenAndServe(":8080", mux)) }