// Package sshx allows git's three verbs out of SSH_ORIGINAL_COMMAND and never uses a shell. 41.3. package sshx import ( "errors" "fmt" "strings" "github.com/barerepo/server/internal/gitx" ) // Request is a parsed SSH_ORIGINAL_COMMAND. type Request struct { Verb string // git-upload-pack, git-receive-pack, git-upload-archive Owner string Name string } func (r Request) Write() bool { return r.Verb == "git-receive-pack" } // allowed is a list, not a pattern, because a pattern admits a fourth verb nobody decided on. var allowed = map[string]bool{ "git-upload-pack": true, "git-receive-pack": true, "git-upload-archive": true, } var errRefused = errors.New("barerepo accepts git over this connection and nothing else") // Parse accepts a verb, a space and a quoted path, where `;` and `&&` are only bad names. func Parse(cmd string) (Request, error) { cmd = strings.TrimSpace(cmd) if cmd == "" { return Request{}, fmt.Errorf("this account has no shell. %w", errRefused) } verb, rest, ok := strings.Cut(cmd, " ") if !ok || !allowed[verb] { return Request{}, fmt.Errorf("%q is not a git command. %w", firstWord(cmd), errRefused) } path, err := unquote(strings.TrimSpace(rest)) if err != nil { return Request{}, err } owner, name, err := splitPath(path) if err != nil { return Request{}, err } return Request{Verb: verb, Owner: owner, Name: name}, nil } // unquote refuses an unquoted argument, because git always quotes the path it sends. func unquote(s string) (string, error) { if len(s) < 2 || s[0] != '\'' || s[len(s)-1] != '\'' { return "", fmt.Errorf("malformed repository argument") } s = s[1 : len(s)-1] if strings.Contains(s, "'") { return "", fmt.Errorf("malformed repository argument") } return s, nil } // splitPath takes an owner and a name from git's four spellings, and builds no filesystem path. func splitPath(p string) (owner, name string, err error) { p = strings.TrimPrefix(p, "~") p = strings.TrimPrefix(p, "/") p = strings.TrimSuffix(p, "/") p = strings.TrimSuffix(p, ".git") owner, name, ok := strings.Cut(p, "/") if !ok || strings.Contains(name, "/") { return "", "", fmt.Errorf("a repository is /, got %q", p) } if !gitx.ValidName(owner) || !gitx.ValidRepoName(name) { return "", "", fmt.Errorf("no such repository") } return owner, name, nil } func firstWord(s string) string { if w, _, ok := strings.Cut(s, " "); ok { return w } return s }