Author | Jakob Wakeling <[email protected]> |
Date | 2025-01-04 00:20:23 |
Commit | 77702b3600a7874e489fd943eeb51a85ea9fc0cb |
Parent | 5d42ac7588aaad46c0909f8c6585f752cefede52 |
Implement goit-shell for Git over SSH
Diffstat
M | Makefile | | | 1 | + |
A | src/shell/main.go | | | 111 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 112 insertions, 0 deletions
diff --git a/Makefile b/Makefile index 9ccdde7..a205aa4 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ VERSION ?= "dev" build: ## Build the project @go build -ldflags "-X $(MODULE)/res.Version=$(VERSION)" -o ./bin/$(PROGRAM) ./src + @go build -ldflags "-X $(MODULE)/res.Version=$(VERSION)" -o ./bin/$(PROGRAM)-shell ./src/shell image: ## Build the project image @docker build -t $(PROGRAM):$(VERSION) --build-arg version=$(VERSION) . diff --git a/src/shell/main.go b/src/shell/main.go new file mode 100644 index 0000000..c7a5b27 --- /dev/null +++ b/src/shell/main.go @@ -0,0 +1,111 @@ +// Copyright (C) 2025, Jakob Wakeling +// All rights reserved. + +package main + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/Jamozed/Goit/src/goit" + "github.com/Jamozed/Goit/src/util" + _ "github.com/mattn/go-sqlite3" +) + +func main() { + if len(os.Args) < 2 || os.Getenv("SSH_ORIGINAL_COMMAND") == "" { + fmt.Fprint(os.Stderr, "Interactive Goit shell is not enabled.\n") + os.Exit(-1) + } + + username := os.Args[1] + soc := os.Getenv("SSH_ORIGINAL_COMMAND") + + /* Parse the SSH_ORIGINAL_COMMAND value for the direction and repository name. */ + re := regexp.MustCompile(`^(git-upload-pack|git-receive-pack|git-upload-archive) '?/?(.*?)'?$`) + matches := re.FindStringSubmatch(soc) + if len(matches) != 3 || matches[1] == "" { + fmt.Fprintln(os.Stderr, "Unknown command:", soc) + os.Exit(-1) + } + + command := matches[1] + action := util.If(strings.Contains(command, "upload"), "r", "w") + reponame := strings.TrimSuffix(strings.TrimSuffix(matches[2], "/"), ".git") + + if !goit.IsLegal(reponame) { + fmt.Fprintln(os.Stderr, "Repository not found.") + os.Exit(-1) + } + + /* Load the config. */ + conf, err := goit.LoadConfig() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to load config:", err.Error()) + os.Exit(-1) + } + + /* Open the database. */ + db, err := sql.Open("sqlite3", filepath.Join(conf.DataPath, "goit.db?_timeout=5000")) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to open database:", err.Error()) + os.Exit(-1) + } + defer db.Close() + + /* Set global config and database variables correctly. */ + goit.Conf = conf + goit.ShellSetDB(db) + + /* Confirm that the database is the expected version. */ + var version int + if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil { + fmt.Fprintln(os.Stderr, "Failed to get database version:", err.Error()) + os.Exit(-1) + } + if version != goit.LATEST_VERSION { + fmt.Fprintln(os.Stderr, "Unexpected database version.") + os.Exit(-1) + } + + /* Confirm that the user exists and is enabled. */ + user, err := goit.GetUserByName(username) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to get user:", err.Error()) + os.Exit(-1) + } + if user == nil { + fmt.Fprintln(os.Stderr, "User not found.") + os.Exit(-1) + } + + /* Confirm that the repository exists. */ + repo, err := goit.GetRepoByName(reponame) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to get repository:", err.Error()) + os.Exit(-1) + } + + /* Check user authorization against repository. */ + if repo == nil || (repo.Visibility == goit.Private && user.Id != repo.OwnerId) { + fmt.Fprintln(os.Stderr, "Repository not found.") + os.Exit(-1) + } + + /* Check if user has write permissions for the repository, if required. */ + if action == "w" && repo.OwnerId != user.Id { + fmt.Fprintln(os.Stderr, "Write access denied.") + os.Exit(-1) + } + + /* Run git-shell command. */ + if _, _, err := goit.NewGitCommand( + "shell", "-c", command+" '"+goit.RepoPath(reponame, true)+"'", + ).Run(os.Stdin, os.Stdout); err != nil { + os.Exit(-1) + } +}