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}