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