Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-19 11:43:37 |
Commit | 6727af8703164d18b368d911ac94e4b7a7df9d7a |
Parent | 17c34f47738b5ccbed6a6f6906e167c673e962b4 |
Implement authentication for Git HTTP operations
Diffstat
M | main.go | | | 2 | +- |
M | src/git.go | | | 128 | +++++++++++++++++++++++++++++++++++++++++++++----------------------------------- |
M | src/goit.go | | | 10 | +++++----- |
M | src/http.go | | | 4 | ++++ |
M | src/user.go | | | 15 | +++++++++++++++ |
5 files changed, 97 insertions, 62 deletions
diff --git a/main.go b/main.go index 0f97b7b..28a77e2 100644 --- a/main.go +++ b/main.go @@ -57,7 +57,7 @@ func main() { } }() - if err := http.ListenAndServe(":8080", logHttp(h)); err != nil { + if err := http.ListenAndServe(goit.Conf.HttpAddr+":"+goit.Conf.HttpPort, logHttp(h)); err != nil { log.Fatalln("[HTTP]", err) } } diff --git a/src/git.go b/src/git.go index 03dc2b2..df2d8c8 100644 --- a/src/git.go +++ b/src/git.go @@ -18,7 +18,7 @@ import ( "github.com/gorilla/mux" ) -type GitCommand struct { +type gitCommand struct { prog string args []string dir string @@ -28,24 +28,19 @@ type GitCommand struct { func HandleInfoRefs(w http.ResponseWriter, r *http.Request) { service := r.FormValue("service") - if service == "git-upload-pack" && r.Header.Get("Git-Protocol") != "version=2" { - HttpError(w, http.StatusForbidden) - return - } - - repo := httpBase(w, r, service) + repo := gitHttpBase(w, r, service) if repo == nil { return } - c := NewCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", "--advertise-refs", ".") - c.AddEnv(os.Environ()...) - c.AddEnv("GIT_PROTOCOL=version=2") - c.dir = "./" + repo.Name + ".git" + c := newCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", "--advertise-refs", ".") + c.addEnv(os.Environ()...) + c.addEnv("GIT_PROTOCOL=version=2") + c.dir = GetRepoPath(repo.Name) - refs, _, err := c.Run(nil, nil) + refs, _, err := c.run(nil, nil) if err != nil { - log.Println("[Git]", err.Error()) + log.Println("[Git HTTP]", err.Error()) HttpError(w, http.StatusInternalServerError) return } @@ -62,70 +57,95 @@ func HandleInfoRefs(w http.ResponseWriter, r *http.Request) { } func HandleUploadPack(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Git-Protocol") != "version=2" { - HttpError(w, http.StatusForbidden) - return - } + const service = "git-upload-pack" - repo := httpBase(w, r, "git-upload-pack") + repo := gitHttpBase(w, r, service) if repo == nil { return } - serviceRPC(w, r, "git-upload-pack", repo) + gitHttpRpc(w, r, service, repo) } func HandleReceivePack(w http.ResponseWriter, r *http.Request) { - repo := httpBase(w, r, "git-receive-pack") + const service = "git-receive-pack" + + repo := gitHttpBase(w, r, service) if repo == nil { return } - serviceRPC(w, r, "git-receive-pack", repo) + gitHttpRpc(w, r, service, repo) } -func httpBase(w http.ResponseWriter, r *http.Request, service string) *Repo { +func gitHttpBase(w http.ResponseWriter, r *http.Request, service string) *Repo { reponame := mux.Vars(r)["repo"] - var isPull bool - switch service { - case "git-upload-pack": - isPull = true - case "git-receive-pack": - isPull = false - default: - HttpError(w, http.StatusNotFound) + /* Check that the Git service and protocol version are supported */ + if service != "git-upload-pack" && service != "git-receive-pack" { + w.WriteHeader(http.StatusForbidden) + return nil + } + if service == "git-upload-pack" && r.Header.Get("Git-Protocol") != "version=2" { + w.WriteHeader(http.StatusForbidden) return nil } + /* Load the repository from the database */ repo, err := GetRepoByName(db, reponame) if err != nil { - HttpError(w, http.StatusInternalServerError) - return nil - } else if repo == nil { - HttpError(w, http.StatusNotFound) + log.Println("[Git HTTP]", err.Error()) + w.WriteHeader(http.StatusInternalServerError) return nil } /* Require authentication other than for public pull */ - if repo.IsPrivate || !isPull { - /* TODO authentcate */ - // HttpError(w, http.StatusUnauthorized) - // return nil + if repo == nil || repo.IsPrivate || service == "git-receive-pack" { + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", "Basic realm=\"git\"") + w.WriteHeader(http.StatusUnauthorized) + return nil + } + + user, err := GetUserByName(username) + if err != nil { + log.Println("[Git HTTP]", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return nil + } + + /* If the user doesn't exist or has invalid credentials */ + if user == nil || !bytes.Equal(Hash(password, user.Salt), user.Pass) { + w.Header().Set("WWW-Authenticate", "Basic realm=\"git\"") + w.WriteHeader(http.StatusUnauthorized) + return nil + } + + /* If the repo doesn't exist or isn't owned by the user */ + if repo == nil || user.Id != repo.OwnerId { + w.WriteHeader(http.StatusNotFound) + return nil + } + } + + if repo == nil { + w.WriteHeader(http.StatusNotFound) + return nil } return repo } -func serviceRPC(w http.ResponseWriter, r *http.Request, service string, repo *Repo) { +func gitHttpRpc(w http.ResponseWriter, r *http.Request, service string, repo *Repo) { defer func() { if err := r.Body.Close(); err != nil { - log.Println("[GitRPC]", err.Error()) + log.Println("[Git RPC]", err.Error()) } }() if r.Header.Get("Content-Type") != "application/x-"+service+"-request" { - log.Println("[GitRPC]", "Content-Type mismatch") + log.Println("[Git RPC]", "Content-Type mismatch") HttpError(w, http.StatusUnauthorized) return } @@ -133,7 +153,7 @@ func serviceRPC(w http.ResponseWriter, r *http.Request, service string, repo *Re body := r.Body if r.Header.Get("Content-Encoding") == "gzip" { if b, err := gzip.NewReader(r.Body); err != nil { - log.Println("[GitRPC]", err.Error()) + log.Println("[Git RPC]", err.Error()) HttpError(w, http.StatusInternalServerError) return } else { @@ -141,19 +161,19 @@ func serviceRPC(w http.ResponseWriter, r *http.Request, service string, repo *Re } } - c := NewCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", ".") - c.AddEnv(os.Environ()...) - c.dir = "./" + repo.Name + ".git" + c := newCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", ".") + c.addEnv(os.Environ()...) + c.dir = GetRepoPath(repo.Name) if p := r.Header.Get("Git-Protocol"); p == "version=2" { - c.AddEnv("GIT_PROTOCOL=version=2") + c.addEnv("GIT_PROTOCOL=version=2") } w.Header().Add("Content-Type", "application/x-"+service+"-result") w.WriteHeader(http.StatusOK) - if _, _, err := c.Run(body, w); err != nil { - log.Println("[GitRPC]", err.Error()) + if _, _, err := c.run(body, w); err != nil { + log.Println("[Git RPC]", err.Error()) HttpError(w, http.StatusInternalServerError) return } @@ -168,19 +188,15 @@ func pktLine(str string) []byte { func pktFlush() []byte { return []byte("0000") } -func NewCommand(args ...string) *GitCommand { - return &GitCommand{prog: "git", args: args} -} - -func (C *GitCommand) AddArgs(args ...string) { - C.args = append(C.args, args...) +func newCommand(args ...string) *gitCommand { + return &gitCommand{prog: "git", args: args} } -func (C *GitCommand) AddEnv(env ...string) { +func (C *gitCommand) addEnv(env ...string) { C.env = append(C.env, env...) } -func (C *GitCommand) Run(in io.Reader, out io.Writer) ([]byte, []byte, error) { +func (C *gitCommand) run(in io.Reader, out io.Writer) ([]byte, []byte, error) { c := exec.Command(C.prog, C.args...) c.Dir = C.dir c.Env = C.env diff --git a/src/goit.go b/src/goit.go index 22634d0..d14a58e 100644 --- a/src/goit.go +++ b/src/goit.go @@ -23,7 +23,7 @@ type Config struct { GitPath string `json:"git_path"` } -var config = Config{ +var Conf = Config{ DataPath: ".", HttpAddr: "", HttpPort: "8080", @@ -40,12 +40,12 @@ func InitGoit(conf string) (err error) { return fmt.Errorf("[Config] %w", err) } } else if dat != nil { - if json.Unmarshal(dat, &config); err != nil { + if json.Unmarshal(dat, &Conf); err != nil { return fmt.Errorf("[Config] %w", err) } } - if dat, err := os.ReadFile(path.Join(config.DataPath, "favicon.png")); err != nil { + if dat, err := os.ReadFile(path.Join(Conf.DataPath, "favicon.png")); err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("[Config] %w", err) } @@ -53,7 +53,7 @@ func InitGoit(conf string) (err error) { Favicon = dat } - if db, err = sql.Open("sqlite3", path.Join(config.DataPath, "goit.db")); err != nil { + if db, err = sql.Open("sqlite3", path.Join(Conf.DataPath, "goit.db")); err != nil { return fmt.Errorf("[Database] %w", err) } @@ -106,5 +106,5 @@ func InitGoit(conf string) (err error) { } func GetRepoPath(name string) string { - return path.Join(config.DataPath, "repos", name+".git") + return path.Join(Conf.DataPath, "repos", name+".git") } diff --git a/src/http.go b/src/http.go index 37cb9df..b3c293c 100644 --- a/src/http.go +++ b/src/http.go @@ -1,3 +1,7 @@ +// http.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + package goit import ( diff --git a/src/user.go b/src/user.go index 237f942..f60fe7f 100644 --- a/src/user.go +++ b/src/user.go @@ -103,6 +103,21 @@ func GetUser(id uint64) (*User, error) { } } +func GetUserByName(name string) (*User, error) { + u := &User{} + + err := db.QueryRow( + "SELECT id, name, name_full, pass, pass_algo, salt, is_admin FROM users WHERE name = ?", strings.ToLower(name), + ).Scan(&u.Id, &u.Name, &u.NameFull, &u.Pass, &u.PassAlgo, &u.Salt, &u.IsAdmin) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err + } + + return u, nil +} + func UserExists(name string) (bool, error) { if err := db.QueryRow("SELECT name FROM users WHERE name = ?", strings.ToLower(name)).Scan(&name); err != nil { if !errors.Is(err, sql.ErrNoRows) {