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}