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}