forked from tangled.org/core
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 "strings" 13 "time" 14 15 securejoin "github.com/cyphar/filepath-securejoin" 16 "github.com/sotangled/tangled/appview" 17) 18 19var ( 20 logger *log.Logger 21 logFile *os.File 22 clientIP string 23 24 // Command line flags 25 incomingUser = flag.String("user", "", "Allowed git user") 26 baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories") 27 logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file") 28 endpoint = flag.String("internal-api", "http://localhost:5444", "Internal API endpoint") 29) 30 31func main() { 32 flag.Parse() 33 34 defer cleanup() 35 initLogger() 36 37 // Get client IP from SSH environment 38 if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" { 39 parts := strings.Fields(connInfo) 40 if len(parts) > 0 { 41 clientIP = parts[0] 42 } 43 } 44 45 if *incomingUser == "" { 46 exitWithLog("access denied: no user specified") 47 } 48 49 sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND") 50 51 logEvent("Connection attempt", map[string]interface{}{ 52 "user": *incomingUser, 53 "command": sshCommand, 54 "client": clientIP, 55 }) 56 57 if sshCommand == "" { 58 exitWithLog("access denied: we don't serve interactive shells :)") 59 } 60 61 cmdParts := strings.Fields(sshCommand) 62 if len(cmdParts) < 2 { 63 exitWithLog("invalid command format") 64 } 65 66 gitCommand := cmdParts[0] 67 68 // did:foo/repo-name or 69 // handle/repo-name 70 71 components := strings.Split(strings.Trim(cmdParts[1], "'"), "/") 72 logEvent("Command components", map[string]interface{}{ 73 "components": components, 74 }) 75 if len(components) != 2 { 76 exitWithLog("invalid repo format, needs <user>/<repo>") 77 } 78 79 didOrHandle := components[0] 80 did := resolveToDid(didOrHandle) 81 repoName := components[1] 82 qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName) 83 84 validCommands := map[string]bool{ 85 "git-receive-pack": true, 86 "git-upload-pack": true, 87 "git-upload-archive": true, 88 } 89 if !validCommands[gitCommand] { 90 exitWithLog("access denied: invalid git command") 91 } 92 93 if gitCommand != "git-upload-pack" { 94 if !isPushPermitted(*incomingUser, qualifiedRepoName) { 95 logEvent("all infos", map[string]interface{}{ 96 "did": *incomingUser, 97 "reponame": qualifiedRepoName, 98 }) 99 exitWithLog("access denied: user not allowed") 100 } 101 } 102 103 fullPath, _ := securejoin.SecureJoin(*baseDirFlag, qualifiedRepoName) 104 105 logEvent("Processing command", map[string]interface{}{ 106 "user": *incomingUser, 107 "command": gitCommand, 108 "repo": repoName, 109 "fullPath": fullPath, 110 "client": clientIP, 111 }) 112 113 if gitCommand == "git-upload-pack" { 114 fmt.Fprintf(os.Stderr, "\x02%s\n", "Welcome to this knot!") 115 } else { 116 fmt.Fprintf(os.Stderr, "%s\n", "Welcome to this knot!") 117 } 118 119 cmd := exec.Command(gitCommand, fullPath) 120 cmd.Stdout = os.Stdout 121 cmd.Stderr = os.Stderr 122 cmd.Stdin = os.Stdin 123 124 if err := cmd.Run(); err != nil { 125 exitWithLog(fmt.Sprintf("command failed: %v", err)) 126 } 127 128 logEvent("Command completed", map[string]interface{}{ 129 "user": *incomingUser, 130 "command": gitCommand, 131 "repo": repoName, 132 "success": true, 133 }) 134} 135 136func resolveToDid(didOrHandle string) string { 137 resolver := appview.NewResolver() 138 ident, err := resolver.ResolveIdent(context.Background(), didOrHandle) 139 if err != nil { 140 exitWithLog(fmt.Sprintf("error resolving handle: %v", err)) 141 } 142 143 // did:plc:foobarbaz/repo 144 return ident.DID.String() 145} 146 147func initLogger() { 148 var err error 149 logFile, err = os.OpenFile(*logPathFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) 150 if err != nil { 151 fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err) 152 os.Exit(1) 153 } 154 155 logger = log.New(logFile, "", 0) 156} 157 158func logEvent(event string, fields map[string]interface{}) { 159 entry := fmt.Sprintf( 160 "timestamp=%q event=%q", 161 time.Now().Format(time.RFC3339), 162 event, 163 ) 164 165 for k, v := range fields { 166 entry += fmt.Sprintf(" %s=%q", k, v) 167 } 168 169 logger.Println(entry) 170} 171 172func exitWithLog(message string) { 173 logEvent("Access denied", map[string]interface{}{ 174 "error": message, 175 }) 176 logFile.Sync() 177 fmt.Fprintf(os.Stderr, "error: %s\n", message) 178 os.Exit(1) 179} 180 181func cleanup() { 182 if logFile != nil { 183 logFile.Sync() 184 logFile.Close() 185 } 186} 187 188func isPushPermitted(user, qualifiedRepoName string) bool { 189 u, _ := url.Parse(*endpoint + "/push-allowed") 190 q := u.Query() 191 q.Add("user", user) 192 q.Add("repo", qualifiedRepoName) 193 u.RawQuery = q.Encode() 194 195 req, err := http.Get(u.String()) 196 if err != nil { 197 exitWithLog(fmt.Sprintf("error verifying permissions: %v", err)) 198 } 199 200 logEvent("url", map[string]interface{}{ 201 "url": u.String(), 202 "status": req.Status, 203 }) 204 205 return req.StatusCode == http.StatusNoContent 206}