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 cmd := exec.Command(gitCommand, fullPath)
116 cmd.Stdout = os.Stdout
117 cmd.Stderr = os.Stderr
118 cmd.Stdin = os.Stdin
119
120 if err := cmd.Run(); err != nil {
121 exitWithLog(fmt.Sprintf("command failed: %v", err))
122 }
123
124 logEvent("Command completed", map[string]interface{}{
125 "user": *incomingUser,
126 "command": gitCommand,
127 "repo": repoName,
128 "success": true,
129 })
130}
131
132func resolveToDid(didOrHandle string) string {
133 ident, err := auth.ResolveIdent(context.Background(), didOrHandle)
134 if err != nil {
135 exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
136 }
137
138 // did:plc:foobarbaz/repo
139 return ident.DID.String()
140}
141
142func handleToDid(handlePath string) string {
143 handle := path.Dir(handlePath)
144
145 ident, err := auth.ResolveIdent(context.Background(), handle)
146 if err != nil {
147 exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
148 }
149
150 // did:plc:foobarbaz/repo
151 didPath := filepath.Join(ident.DID.String(), path.Base(handlePath))
152
153 return didPath
154}
155
156func initLogger() {
157 var err error
158 logFile, err = os.OpenFile(*logPathFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
159 if err != nil {
160 fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err)
161 os.Exit(1)
162 }
163
164 logger = log.New(logFile, "", 0)
165}
166
167func logEvent(event string, fields map[string]interface{}) {
168 entry := fmt.Sprintf(
169 "timestamp=%q event=%q",
170 time.Now().Format(time.RFC3339),
171 event,
172 )
173
174 for k, v := range fields {
175 entry += fmt.Sprintf(" %s=%q", k, v)
176 }
177
178 logger.Println(entry)
179}
180
181func exitWithLog(message string) {
182 logEvent("Access denied", map[string]interface{}{
183 "error": message,
184 })
185 logFile.Sync()
186 fmt.Fprintf(os.Stderr, "error: %s\n", message)
187 os.Exit(1)
188}
189
190func cleanup() {
191 if logFile != nil {
192 logFile.Sync()
193 logFile.Close()
194 }
195}
196
197func isPushPermitted(user, qualifiedRepoName string) bool {
198 u, _ := url.Parse(*endpoint + "/push-allowed")
199 q := u.Query()
200 q.Add("user", user)
201 q.Add("repo", qualifiedRepoName)
202 u.RawQuery = q.Encode()
203
204 req, err := http.Get(u.String())
205 if err != nil {
206 exitWithLog(fmt.Sprintf("error verifying permissions: %v", err))
207 }
208
209 logEvent("url", map[string]interface{}{
210 "url": u.String(),
211 "status": req.Status,
212 })
213
214 return req.StatusCode == http.StatusNoContent
215}