this repo has no description
1package main
2
3import (
4 "context"
5 "flag"
6 "fmt"
7 "log"
8 "os"
9 "os/exec"
10 "path"
11 "path/filepath"
12 "strings"
13 "time"
14
15 "github.com/sotangled/tangled/appview/auth"
16)
17
18var (
19 logger *log.Logger
20 logFile *os.File
21 clientIP string
22
23 // Command line flags
24 allowedUser = flag.String("user", "", "Allowed git user")
25 baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories")
26 logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file")
27)
28
29func main() {
30 flag.Parse()
31
32 defer cleanup()
33 initLogger()
34
35 // Get client IP from SSH environment
36 if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" {
37 parts := strings.Fields(connInfo)
38 if len(parts) > 0 {
39 clientIP = parts[0]
40 }
41 }
42
43 if *allowedUser == "" {
44 exitWithLog("access denied: no user specified")
45 }
46
47 sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
48
49 logEvent("Connection attempt", map[string]interface{}{
50 "user": *allowedUser,
51 "command": sshCommand,
52 "client": clientIP,
53 })
54
55 if sshCommand == "" {
56 exitWithLog("access denied: we don't serve interactive shells :)")
57 }
58
59 cmdParts := strings.Fields(sshCommand)
60 if len(cmdParts) < 2 {
61 exitWithLog("invalid command format")
62 }
63
64 gitCommand := cmdParts[0]
65
66 // example.com/repo
67 handlePath := strings.Trim(cmdParts[1], "'")
68 repoName := handleToDID(handlePath)
69
70 validCommands := map[string]bool{
71 "git-receive-pack": true,
72 "git-upload-pack": true,
73 "git-upload-archive": true,
74 }
75 if !validCommands[gitCommand] {
76 exitWithLog("access denied: invalid git command")
77 }
78
79 did := path.Dir(repoName)
80 if gitCommand != "git-upload-pack" {
81 if !isAllowedUser(*allowedUser, did) {
82 exitWithLog("access denied: user not allowed")
83 }
84 }
85
86 fullPath := filepath.Join(*baseDirFlag, repoName)
87 fullPath = filepath.Clean(fullPath)
88
89 logEvent("Processing command", map[string]interface{}{
90 "user": *allowedUser,
91 "command": gitCommand,
92 "repo": repoName,
93 "fullPath": fullPath,
94 "client": clientIP,
95 })
96
97 cmd := exec.Command(gitCommand, fullPath)
98 cmd.Stdout = os.Stdout
99 cmd.Stderr = os.Stderr
100 cmd.Stdin = os.Stdin
101
102 if err := cmd.Run(); err != nil {
103 exitWithLog(fmt.Sprintf("command failed: %v", err))
104 }
105
106 logEvent("Command completed", map[string]interface{}{
107 "user": *allowedUser,
108 "command": gitCommand,
109 "repo": repoName,
110 "success": true,
111 })
112}
113
114func handleToDID(handlePath string) string {
115 handle := path.Dir(handlePath)
116
117 ident, err := auth.ResolveIdent(context.Background(), handle)
118 if err != nil {
119 exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
120 }
121
122 // did:plc:foobarbaz/repo
123 didPath := filepath.Join(ident.DID.String(), path.Base(handlePath))
124
125 return didPath
126}
127
128func initLogger() {
129 var err error
130 logFile, err = os.OpenFile(*logPathFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
131 if err != nil {
132 fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err)
133 os.Exit(1)
134 }
135
136 logger = log.New(logFile, "", 0)
137}
138
139func logEvent(event string, fields map[string]interface{}) {
140 entry := fmt.Sprintf(
141 "timestamp=%q event=%q",
142 time.Now().Format(time.RFC3339),
143 event,
144 )
145
146 for k, v := range fields {
147 entry += fmt.Sprintf(" %s=%q", k, v)
148 }
149
150 logger.Println(entry)
151}
152
153func exitWithLog(message string) {
154 logEvent("Access denied", map[string]interface{}{
155 "error": message,
156 })
157 logFile.Sync()
158 fmt.Fprintf(os.Stderr, "error: %s\n", message)
159 os.Exit(1)
160}
161
162func cleanup() {
163 if logFile != nil {
164 logFile.Sync()
165 logFile.Close()
166 }
167}
168
169func isAllowedUser(user, did string) bool {
170 return user == did
171}