File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/sshx/command.go
85 lines · 2.4kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package sshx allows git's three verbs out of SSH_ORIGINAL_COMMAND and never uses a shell. 41.3.
133728e barerepo 1mo
2
package sshx
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"errors"
133728e barerepo 1mo
6
"fmt"
133728e barerepo 1mo
7
"strings"
133728e barerepo 1mo
8
133728e barerepo 1mo
9
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
10
)
133728e barerepo 1mo
11
133728e barerepo 1mo
12
// Request is a parsed SSH_ORIGINAL_COMMAND.
133728e barerepo 1mo
13
type Request struct {
133728e barerepo 1mo
14
Verb string // git-upload-pack, git-receive-pack, git-upload-archive
133728e barerepo 1mo
15
Owner string
133728e barerepo 1mo
16
Name string
133728e barerepo 1mo
17
}
133728e barerepo 1mo
18
133728e barerepo 1mo
19
func (r Request) Write() bool { return r.Verb == "git-receive-pack" }
133728e barerepo 1mo
20
133728e barerepo 1mo
21
// allowed is a list, not a pattern, because a pattern admits a fourth verb nobody decided on.
133728e barerepo 1mo
22
var allowed = map[string]bool{
133728e barerepo 1mo
23
"git-upload-pack": true,
133728e barerepo 1mo
24
"git-receive-pack": true,
133728e barerepo 1mo
25
"git-upload-archive": true,
133728e barerepo 1mo
26
}
133728e barerepo 1mo
27
133728e barerepo 1mo
28
var errRefused = errors.New("barerepo accepts git over this connection and nothing else")
133728e barerepo 1mo
29
133728e barerepo 1mo
30
// Parse accepts a verb, a space and a quoted path, where `;` and `&&` are only bad names.
133728e barerepo 1mo
31
func Parse(cmd string) (Request, error) {
133728e barerepo 1mo
32
cmd = strings.TrimSpace(cmd)
133728e barerepo 1mo
33
if cmd == "" {
133728e barerepo 1mo
34
return Request{}, fmt.Errorf("this account has no shell. %w", errRefused)
133728e barerepo 1mo
35
}
133728e barerepo 1mo
36
verb, rest, ok := strings.Cut(cmd, " ")
133728e barerepo 1mo
37
if !ok || !allowed[verb] {
133728e barerepo 1mo
38
return Request{}, fmt.Errorf("%q is not a git command. %w", firstWord(cmd), errRefused)
133728e barerepo 1mo
39
}
133728e barerepo 1mo
40
path, err := unquote(strings.TrimSpace(rest))
133728e barerepo 1mo
41
if err != nil {
133728e barerepo 1mo
42
return Request{}, err
133728e barerepo 1mo
43
}
133728e barerepo 1mo
44
owner, name, err := splitPath(path)
133728e barerepo 1mo
45
if err != nil {
133728e barerepo 1mo
46
return Request{}, err
133728e barerepo 1mo
47
}
133728e barerepo 1mo
48
return Request{Verb: verb, Owner: owner, Name: name}, nil
133728e barerepo 1mo
49
}
133728e barerepo 1mo
50
133728e barerepo 1mo
51
// unquote refuses an unquoted argument, because git always quotes the path it sends.
133728e barerepo 1mo
52
func unquote(s string) (string, error) {
133728e barerepo 1mo
53
if len(s) < 2 || s[0] != '\'' || s[len(s)-1] != '\'' {
133728e barerepo 1mo
54
return "", fmt.Errorf("malformed repository argument")
133728e barerepo 1mo
55
}
133728e barerepo 1mo
56
s = s[1 : len(s)-1]
133728e barerepo 1mo
57
if strings.Contains(s, "'") {
133728e barerepo 1mo
58
return "", fmt.Errorf("malformed repository argument")
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
return s, nil
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
133728e barerepo 1mo
63
// splitPath takes an owner and a name from git's four spellings, and builds no filesystem path.
133728e barerepo 1mo
64
func splitPath(p string) (owner, name string, err error) {
133728e barerepo 1mo
65
p = strings.TrimPrefix(p, "~")
133728e barerepo 1mo
66
p = strings.TrimPrefix(p, "/")
133728e barerepo 1mo
67
p = strings.TrimSuffix(p, "/")
133728e barerepo 1mo
68
p = strings.TrimSuffix(p, ".git")
133728e barerepo 1mo
69
133728e barerepo 1mo
70
owner, name, ok := strings.Cut(p, "/")
133728e barerepo 1mo
71
if !ok || strings.Contains(name, "/") {
133728e barerepo 1mo
72
return "", "", fmt.Errorf("a repository is <account>/<name>, got %q", p)
133728e barerepo 1mo
73
}
133728e barerepo 1mo
74
if !gitx.ValidName(owner) || !gitx.ValidRepoName(name) {
133728e barerepo 1mo
75
return "", "", fmt.Errorf("no such repository")
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
return owner, name, nil
133728e barerepo 1mo
78
}
133728e barerepo 1mo
79
133728e barerepo 1mo
80
func firstWord(s string) string {
133728e barerepo 1mo
81
if w, _, ok := strings.Cut(s, " "); ok {
133728e barerepo 1mo
82
return w
133728e barerepo 1mo
83
}
133728e barerepo 1mo
84
return s
133728e barerepo 1mo
85
}
history · rawbarerepo 0.1.0