Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-19 07:53:59 |
Commit | 1828e5fb2957c068734703ded8c0b6144b9bba3d |
Parent | 25ea845fd12009e041cf228fe3225ef0c27f15f7 |
Enable Git receive-pack service
Diffstat
M | main.go | | | 28 | +++++++++++++--------------- |
M | res/repo_log.html | | | 89 | +++++++++++++++++++++++++++++++++++++++++++++---------------------------------- |
M | res/style.css | | | 4 | +++- |
M | src/admin.go | | | 14 | +++++++------- |
M | src/git.go | | | 25 | +++++++++++++++++-------- |
M | src/goit.go | | | 55 | ++++++++++++++++++++++++++++++++++++++----------------- |
M | src/repo.go | | | 18 | +++++++++--------- |
M | src/user.go | | | 16 | ++++++++-------- |
M | src/util.go | | | 4 | +++- |
9 files changed, 149 insertions, 104 deletions
diff --git a/main.go b/main.go index 0ef7922..63f947d 100644 --- a/main.go +++ b/main.go @@ -16,33 +16,30 @@ import ( ) func main() { - g, err := goit.InitGoit() - if err != nil { + if err := goit.InitGoit("./goit.json"); err != nil { log.Fatalln("[InitGoit]", err.Error()) - } else { - defer g.Close() } h := mux.NewRouter() h.StrictSlash(true) - h.Path("/").HandlerFunc(g.HandleIndex) - h.Path("/user/login").Methods("GET", "POST").HandlerFunc(g.HandleUserLogin) - h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(g.HandleUserLogout) + h.Path("/").HandlerFunc(goit.HandleIndex) + h.Path("/user/login").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogin) + h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogout) // h.Path("/user/settings").Methods("GET").HandlerFunc() - h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(g.HandleRepoCreate) + h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(goit.HandleRepoCreate) // h.Path("/repo/delete").Methods("POST").HandlerFunc() // h.Path("/admin/settings").Methods("GET").HandlerFunc() - h.Path("/admin/user").Methods("GET").HandlerFunc(g.HandleAdminUserIndex) + h.Path("/admin/user").Methods("GET").HandlerFunc(goit.HandleAdminUserIndex) // h.Path("/admin/repos").Methods("GET").HandlerFunc() - h.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(g.HandleAdminUserCreate) + h.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(goit.HandleAdminUserCreate) // h.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc() h.Path("/{repo:.+(?:\\.git)$}").Methods(http.MethodGet).HandlerFunc(redirectDotGit) - h.Path("/{repo}").Methods(http.MethodGet).HandlerFunc(g.HandleRepoLog) - h.Path("/{repo}/log").Methods(http.MethodGet).HandlerFunc(g.HandleRepoLog) - h.Path("/{repo}/tree").Methods(http.MethodGet).HandlerFunc(g.HandleRepoTree) - h.Path("/{repo}/refs").Methods(http.MethodGet).HandlerFunc(g.HandleRepoRefs) + h.Path("/{repo}").Methods(http.MethodGet).HandlerFunc(goit.HandleRepoLog) + h.Path("/{repo}/log").Methods(http.MethodGet).HandlerFunc(goit.HandleRepoLog) + h.Path("/{repo}/tree").Methods(http.MethodGet).HandlerFunc(goit.HandleRepoTree) + h.Path("/{repo}/refs").Methods(http.MethodGet).HandlerFunc(goit.HandleRepoRefs) h.Path("/{repo}/info/refs").Methods(http.MethodGet).HandlerFunc(goit.HandleInfoRefs) h.Path("/{repo}/git-upload-pack").Methods(http.MethodPost).HandlerFunc(goit.HandleUploadPack) h.Path("/{repo}/git-receive-pack").Methods(http.MethodPost).HandlerFunc(goit.HandleReceivePack) @@ -67,6 +64,7 @@ func main() { func logHttp(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("[HTTP]", r.RemoteAddr, r.Method, r.URL.String()) + // log.Println("[HTTP]", r.Header) handler.ServeHTTP(w, r) }) } @@ -74,7 +72,7 @@ func logHttp(handler http.Handler) http.Handler { func handleStyle(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/css") if _, err := w.Write([]byte(res.Style)); err != nil { - log.Println("[handleStyle]", err.Error()) + log.Println("[Style]", err.Error()) } } diff --git a/res/repo_log.html b/res/repo_log.html index 8483e0b..999aa88 100644 --- a/res/repo_log.html +++ b/res/repo_log.html @@ -7,44 +7,57 @@ <link rel="icon" type="image/png" href="/static/favicon.png"> </head> <body> - <table> - <tr><td><h1>{{.Name}}</h1></td></tr> - <tr><td><span>{{.Description}}</span></td></tr> - <tr><td>git clone <a href="{{.Url}}">{{.Url}}</a></td></tr> - <tr> - <td> - <a href="/{{.Name}}/log">Log</a> - | <a href="/{{.Name}}/tree">Tree</a> - | <a href="/{{.Name}}/refs">Refs</a> - {{if .HasReadme}} - | <a href="">README</a> - {{end}} - {{if .HasLicence}} - | <a href="">LICENCE</a> - {{end}} - </td> - </tr> - </table><hr> - {{if .Commits}} + <header> <table> - <thead> - <tr> - <td><b>Date</b></td> - <td><b>Message</b></td> - <td><b>Author</b></td> - </tr> - </thead> - <tbody> - {{range .Commits}} - <tr> - <td>{{.Date}}</a></td> - <td>{{.Message}}</td> - <td>{{.Author}}</td> - </tr> - {{end}} - </tbody> + <tr> + <td rowspan="2"><a href="/"><img style="max-height: 22px;" src=""></a></td> + <td><h1>{{.Name}}</h1></td> + </tr> + <tr> + <td>{{.Description}}</td> + </tr> + <tr> + <td></td> + <td>git clone <a href="{{.Url}}">{{.Url}}</a></td> + </tr> + <tr> + <td></td> + <td> + <a href="/{{.Name}}/log">Log</a> + | <a href="/{{.Name}}/tree">Tree</a> + | <a href="/{{.Name}}/refs">Refs</a> + {{if .HasReadme}} + | <a href="">README</a> + {{end}} + {{if .HasLicence}} + | <a href="">LICENCE</a> + {{end}} + </td> + </tr> </table> - {{else}} - <span>No commits</span> - {{end}} + </header><hr> + <main> + {{if .Commits}} + <table> + <thead> + <tr> + <td><b>Date</b></td> + <td><b>Message</b></td> + <td><b>Author</b></td> + </tr> + </thead> + <tbody> + {{range .Commits}} + <tr> + <td>{{.Date}}</a></td> + <td>{{.Message}}</td> + <td>{{.Author}}</td> + </tr> + {{end}} + </tbody> + </table> + {{else}} + <span>No commits</span> + {{end}} + </main> </body> diff --git a/res/style.css b/res/style.css index efd7049..9f3f842 100644 --- a/res/style.css +++ b/res/style.css @@ -7,4 +7,6 @@ h1, h2 { font-size: 1em; margin: 0; } hr { border: 0; height: 1em; margin: 0; } table td { padding: 0 0.4em; } -table tr:hover td { background-color: #222222; } +table td:empty::after { content: "\00a0"; } + +main table tr:hover td { background-color: #222222; } diff --git a/src/admin.go b/src/admin.go index cb8f762..7d134cf 100644 --- a/src/admin.go +++ b/src/admin.go @@ -22,11 +22,11 @@ func init() { adminUserIndex = template.Must(template.New("admin_user_index").Parse(res.AdminUserIndex)) } -func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) { +func HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) { if ok, uid := AuthHttp(r); !ok { HttpError(w, http.StatusNotFound) return - } else if user, err := g.GetUser(uid); err != nil { + } else if user, err := GetUser(uid); err != nil { log.Println("[Admin:User:Create:Auth]", err.Error()) HttpError(w, http.StatusNotFound) return @@ -35,7 +35,7 @@ func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) { return } - if rows, err := g.db.Query("SELECT id, name, name_full, is_admin FROM users"); err != nil { + if rows, err := db.Query("SELECT id, name, name_full, is_admin FROM users"); err != nil { log.Println("[Admin:User:Index:SELECT]", err.Error()) HttpError(w, http.StatusInternalServerError) } else { @@ -63,11 +63,11 @@ func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) { } } -func (g *Goit) HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { +func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { if ok, uid := AuthHttp(r); !ok { HttpError(w, http.StatusNotFound) return - } else if user, err := g.GetUser(uid); err != nil { + } else if user, err := GetUser(uid); err != nil { log.Println("[Admin:User:Create:Auth]", err.Error()) HttpError(w, http.StatusNotFound) return @@ -88,7 +88,7 @@ func (g *Goit) HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { data.Msg = "Username cannot be empty" } else if SliceContains(reserved, username) { data.Msg = "Username \"" + username + "\" is reserved" - } else if exists, err := g.UserExists(username); err != nil { + } else if exists, err := UserExists(username); err != nil { log.Println("[Admin:User:Create:Exists]", err.Error()) HttpError(w, http.StatusInternalServerError) return @@ -98,7 +98,7 @@ func (g *Goit) HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { log.Println("[Admin:User:Create:Salt]", err.Error()) HttpError(w, http.StatusInternalServerError) return - } else if _, err := g.db.Exec( + } else if _, err := db.Exec( "INSERT INTO users (name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?)", username, fullname, Hash(password, salt), "argon2", salt, admin, ); err != nil { diff --git a/src/git.go b/src/git.go index cd7dcbf..03dc2b2 100644 --- a/src/git.go +++ b/src/git.go @@ -27,6 +27,12 @@ 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) if repo == nil { return @@ -56,6 +62,11 @@ 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 + } + repo := httpBase(w, r, "git-upload-pack") if repo == nil { return @@ -87,11 +98,6 @@ func httpBase(w http.ResponseWriter, r *http.Request, service string) *Repo { return nil } - if r.Header.Get("Git-Protocol") != "version=2" { - HttpError(w, http.StatusForbidden) - return nil - } - repo, err := GetRepoByName(db, reponame) if err != nil { HttpError(w, http.StatusInternalServerError) @@ -104,8 +110,8 @@ func httpBase(w http.ResponseWriter, r *http.Request, service string) *Repo { /* Require authentication other than for public pull */ if repo.IsPrivate || !isPull { /* TODO authentcate */ - HttpError(w, http.StatusUnauthorized) - return nil + // HttpError(w, http.StatusUnauthorized) + // return nil } return repo @@ -137,9 +143,12 @@ 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.AddEnv("GIT_PROTOCOL=version=2") c.dir = "./" + repo.Name + ".git" + if p := r.Header.Get("Git-Protocol"); p == "version=2" { + c.AddEnv("GIT_PROTOCOL=version=2") + } + w.Header().Add("Content-Type", "application/x-"+service+"-result") w.WriteHeader(http.StatusOK) diff --git a/src/goit.go b/src/goit.go index 7c40cdd..7553fc4 100644 --- a/src/goit.go +++ b/src/goit.go @@ -6,28 +6,49 @@ package goit import ( "database/sql" + "encoding/json" + "errors" "fmt" "log" + "os" + "path" _ "github.com/mattn/go-sqlite3" ) -var db *sql.DB +type Config struct { + DataPath string `json:"data_path"` + HttpAddr string `json:"http_addr"` + HttpPort string `json:"http_port"` + GitPath string `json:"git_path"` +} -type Goit struct { - db *sql.DB +var config = Config{ + DataPath: ".", + HttpAddr: "", + HttpPort: "8080", + GitPath: "git", } +var db *sql.DB + /* Initialise Goit. */ -func InitGoit() (g *Goit, err error) { - g = &Goit{} +func InitGoit(conf string) (err error) { + if dat, err := os.ReadFile(conf); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("[Config] %w", err) + } + } else if dat != nil { + if json.Unmarshal(dat, &config); err != nil { + return fmt.Errorf("[Config] %w", err) + } + } - if g.db, err = sql.Open("sqlite3", "./goit.db"); err != nil { - return nil, fmt.Errorf("[SQL:open] %w", err) + if db, err = sql.Open("sqlite3", path.Join(config.DataPath, "goit.db")); err != nil { + return fmt.Errorf("[Database] %w", err) } - db = g.db - if _, err = g.db.Exec( + if _, err = db.Exec( `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, @@ -38,10 +59,10 @@ func InitGoit() (g *Goit, err error) { is_admin BOOLEAN NOT NULL )`, ); err != nil { - return nil, fmt.Errorf("[CREATE:users] %w", err) + return fmt.Errorf("[CREATE:users] %w", err) } - if _, err = g.db.Exec( + if _, err = db.Exec( `CREATE TABLE IF NOT EXISTS repos ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER NOT NULL, @@ -52,18 +73,18 @@ func InitGoit() (g *Goit, err error) { is_private BOOLEAN NOT NULL )`, ); err != nil { - return nil, fmt.Errorf("[CREATE:repos] %w", err) + return fmt.Errorf("[CREATE:repos] %w", err) } /* Create an admin user if one does not exist */ - if exists, err := g.UserExists("admin"); err != nil { + if exists, err := UserExists("admin"); err != nil { log.Println("[admin:Exists]", err.Error()) err = nil /* ignored */ } else if !exists { if salt, err := Salt(); err != nil { log.Println("[admin:Salt]", err.Error()) err = nil /* ignored */ - } else if _, err = g.db.Exec( + } else if _, err = db.Exec( "INSERT INTO users (id, name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?, ?)", 0, "admin", "Administrator", Hash("admin", salt), "argon2", salt, true, ); err != nil { @@ -72,9 +93,9 @@ func InitGoit() (g *Goit, err error) { } } - return g, nil + return nil } -func (g *Goit) Close() error { - return g.db.Close() +func GetRepoPath(username, reponame string) string { + return path.Join(config.DataPath, "repos", username, reponame+".git") } diff --git a/src/repo.go b/src/repo.go index ec35f3f..400e9d2 100644 --- a/src/repo.go +++ b/src/repo.go @@ -38,10 +38,10 @@ var ( repoRefs *template.Template = template.Must(template.New("repo_refs").Parse(res.RepoRefs)) ) -func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) { +func HandleIndex(w http.ResponseWriter, r *http.Request) { authOk, uid := AuthHttp(r) - if rows, err := g.db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil { + if rows, err := db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil { log.Println("[Index:SELECT]", err.Error()) HttpError(w, http.StatusInternalServerError) } else { @@ -56,7 +56,7 @@ func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) { if err := rows.Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.IsPrivate); err != nil { log.Println("[Index:SELECT:Scan]", err.Error()) } else if !r.IsPrivate || (authOk && uid == r.OwnerId) { - owner, err := g.GetUser(r.OwnerId) + owner, err := GetUser(r.OwnerId) if err != nil { log.Println("[Index:SELECT:UserName]", err.Error()) } @@ -74,14 +74,14 @@ func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) { } } -func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) { +func HandleRepoCreate(w http.ResponseWriter, r *http.Request) { if ok, uid := AuthHttp(r); !ok { HttpError(w, http.StatusUnauthorized) } else if r.Method == http.MethodPost { name := r.FormValue("reponame") private := r.FormValue("visibility") == "private" - if taken, err := RepoExists(g.db, name); err != nil { + if taken, err := RepoExists(db, name); err != nil { log.Println("[RepoCreate:RepoExists]", err.Error()) HttpError(w, http.StatusInternalServerError) } else if taken { @@ -89,7 +89,7 @@ func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) { } else if SliceContains[string](reserved, name) { repoCreate.Execute(w, struct{ Msg string }{"Reponame is reserved"}) } else { - if _, err := g.db.Exec( + if _, err := db.Exec( `INSERT INTO repos ( owner_id, name, name_lower, description, default_branch, is_private ) VALUES (?, ?, ?, ?, ?, ?)`, @@ -106,7 +106,7 @@ func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) { } } -func (g *Goit) HandleRepoLog(w http.ResponseWriter, r *http.Request) { +func HandleRepoLog(w http.ResponseWriter, r *http.Request) { reponame := mux.Vars(r)["repo"] repo, err := GetRepoByName(db, reponame) @@ -153,11 +153,11 @@ func (g *Goit) HandleRepoLog(w http.ResponseWriter, r *http.Request) { } } -func (g *Goit) HandleRepoTree(w http.ResponseWriter, r *http.Request) { +func HandleRepoTree(w http.ResponseWriter, r *http.Request) { HttpError(w, http.StatusNoContent) } -func (g *Goit) HandleRepoRefs(w http.ResponseWriter, r *http.Request) { +func HandleRepoRefs(w http.ResponseWriter, r *http.Request) { reponame := mux.Vars(r)["repo"] repo, err := GetRepoByName(db, reponame) diff --git a/src/user.go b/src/user.go index 9b2617c..237f942 100644 --- a/src/user.go +++ b/src/user.go @@ -35,7 +35,7 @@ var ( userCreate *template.Template = template.Must(template.New("user_create").Parse(res.UserCreate)) ) -func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) { +func HandleUserLogin(w http.ResponseWriter, r *http.Request) { if ok, _ := AuthHttp(r); ok { http.Redirect(w, r, "/", http.StatusFound) return @@ -50,13 +50,13 @@ func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) { if username == "" { data.Msg = "Username cannot be empty" - } else if exists, err := g.UserExists(username); err != nil { + } else if exists, err := UserExists(username); err != nil { log.Println("[User:Login:Exists]", err.Error()) HttpError(w, http.StatusInternalServerError) return } else if !exists { data.Msg = "Invalid credentials" - } else if err := g.db.QueryRow( + } else if err := db.QueryRow( "SELECT id, name, pass, pass_algo, salt FROM users WHERE name = ?", username, ).Scan(&u.Id, &u.Name, &u.Pass, &u.PassAlgo, &u.Salt); err != nil { log.Println("[User:Login:SELECT]", err.Error()) @@ -81,16 +81,16 @@ func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) { userLogin.Execute(w, data) } -func (g *Goit) HandleUserLogout(w http.ResponseWriter, r *http.Request) { +func HandleUserLogout(w http.ResponseWriter, r *http.Request) { EndSession(SessionCookie(r)) http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1}) http.Redirect(w, r, "/", http.StatusFound) } -func (g *Goit) GetUser(id uint64) (*User, error) { +func GetUser(id uint64) (*User, error) { u := User{} - if err := g.db.QueryRow( + if err := db.QueryRow( "SELECT id, name, name_full, is_admin FROM users WHERE id = ?", id, ).Scan(&u.Id, &u.Name, &u.NameFull, &u.IsAdmin); err != nil { if !errors.Is(err, sql.ErrNoRows) { @@ -103,8 +103,8 @@ func (g *Goit) GetUser(id uint64) (*User, error) { } } -func (g *Goit) UserExists(name string) (bool, error) { - if err := g.db.QueryRow("SELECT name FROM users WHERE name = ?", strings.ToLower(name)).Scan(&name); err != 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) { return false, err } else { diff --git a/src/util.go b/src/util.go index a273c72..3cd9efc 100644 --- a/src/util.go +++ b/src/util.go @@ -4,7 +4,9 @@ package goit -import "net/http" +import ( + "net/http" +) func If[T any](cond bool, a, b T) T { if cond {