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:5444", "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
72 components := strings.Split(strings.Trim(cmdParts[1], "'"), "/")
73 logEvent("Command components", map[string]interface{}{
74 "components": components,
75 })
76 if len(components) != 2 {
77 exitWithLog("invalid repo format, needs <user>/<repo>")
78 }
79
80 didOrHandle := components[0]
81 did := resolveToDid(didOrHandle)
82 repoName := components[1]
83 qualifiedRepoName := filepath.Join(did, repoName)
84
85 validCommands := map[string]bool{
86 "git-receive-pack": true,
87 "git-upload-pack": true,
88 "git-upload-archive": true,
89 }
90 if !validCommands[gitCommand] {
91 exitWithLog("access denied: invalid git command")
92 }
93
94 if gitCommand != "git-upload-pack" {
95 if !isPushPermitted(*incomingUser, qualifiedRepoName) {
96 logEvent("all infos", map[string]interface{}{
97 "did": *incomingUser,
98 "reponame": qualifiedRepoName,
99 })
100 exitWithLog("access denied: user not allowed")
101 }
102 }
103
104 fullPath := filepath.Join(*baseDirFlag, qualifiedRepoName)
105 fullPath = filepath.Clean(fullPath)
106
107 logEvent("Processing command", map[string]interface{}{
108 "user": *incomingUser,
109 "command": gitCommand,
110 "repo": repoName,
111 "fullPath": fullPath,
112 "client": clientIP,
113 })
114
115 if gitCommand == "git-upload-pack" {
116 fmt.Fprintf(os.Stderr, "\x02%s\n", "Welcome to this knot!")
117 } else {
118 fmt.Fprintf(os.Stderr, "%s\n", "Welcome to this knot!")
119 }
120
121 cmd := exec.Command(gitCommand, fullPath)
122 cmd.Stdout = os.Stdout
123 cmd.Stderr = os.Stderr
124 cmd.Stdin = os.Stdin
125
126 if err := cmd.Run(); err != nil {
127 exitWithLog(fmt.Sprintf("command failed: %v", err))
128 }
129
130 logEvent("Command completed", map[string]interface{}{
131 "user": *incomingUser,
132 "command": gitCommand,
133 "repo": repoName,
134 "success": true,
135 })
136}
137
138func resolveToDid(didOrHandle string) string {
139 ident, err := auth.ResolveIdent(context.Background(), didOrHandle)
140 if err != nil {
141 exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
142 }
143
144 // did:plc:foobarbaz/repo
145 return ident.DID.String()
146}
147
148func handleToDid(handlePath string) string {
149 handle := path.Dir(handlePath)
150
151 ident, err := auth.ResolveIdent(context.Background(), handle)
152 if err != nil {
153 exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
154 }
155
156 // did:plc:foobarbaz/repo
157 didPath := filepath.Join(ident.DID.String(), path.Base(handlePath))
158
159 return didPath
160}
161
162func initLogger() {
163 var err error
164 logFile, err = os.OpenFile(*logPathFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
165 if err != nil {
166 fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err)
167 os.Exit(1)
168 }
169
170 logger = log.New(logFile, "", 0)
171}
172
173func logEvent(event string, fields map[string]interface{}) {
174 entry := fmt.Sprintf(
175 "timestamp=%q event=%q",
176 time.Now().Format(time.RFC3339),
177 event,
178 )
179
180 for k, v := range fields {
181 entry += fmt.Sprintf(" %s=%q", k, v)
182 }
183
184 logger.Println(entry)
185}
186
187func exitWithLog(message string) {
188 logEvent("Access denied", map[string]interface{}{
189 "error": message,
190 })
191 logFile.Sync()
192 fmt.Fprintf(os.Stderr, "error: %s\n", message)
193 os.Exit(1)
194}
195
196func cleanup() {
197 if logFile != nil {
198 logFile.Sync()
199 logFile.Close()
200 }
201}
202
203func isPushPermitted(user, qualifiedRepoName string) bool {
204 u, _ := url.Parse(*endpoint + "/push-allowed")
205 q := u.Query()
206 q.Add("user", user)
207 q.Add("repo", qualifiedRepoName)
208 u.RawQuery = q.Encode()
209
210 req, err := http.Get(u.String())
211 if err != nil {
212 exitWithLog(fmt.Sprintf("error verifying permissions: %v", err))
213 }
214
215 logEvent("url", map[string]interface{}{
216 "url": u.String(),
217 "status": req.Status,
218 })
219
220 return req.StatusCode == http.StatusNoContent
221}