package main import ( "context" "fmt" "io" "log" "os" "os/exec" "regexp" "sort" "sync" ssh "golang.org/x/crypto/ssh" ) const ( DATAFLOW_INVALID = iota DATAFLOW_GIT_UPLOAD_PACK DATAFLOW_GIT_RECEIVE_PACK ) func (srv *GruauSSHServer) sessionGit(ctx context.Context, auth *Auth, pubkeyfp string, channel ssh.Channel, requestC <-chan *ssh.Request) error { var mux sync.Mutex envValues := map[string]string{ "GIT_PROTOCOL": "version=2", "LANG": "en_US.UTF-8", } var executing bool exited := make(chan int, 1) exit := func(code int) { go func() { exited <- code }() } for { select { case req, ok := <-requestC: if !ok { return ErrExhaustedRequestChannel } switch req.Type { case "env": reply(req, func() bool { name, value, ok := UnmarshalSSHPacketEnv(req.Payload) if !ok { return false } mux.Lock() defer mux.Unlock() if executing { return false } // only allow known environment variables // (those that have an already set default) _, ok = envValues[name] if !ok { return false } envValues[name] = value return true }()) case "exec": command, ok := UnmarshalSSHPacketExec(req.Payload) if !ok { reply(req, false) close(exited) break } reply(req, true) // from now on, the exec command "succeeded" in a protocol sense; all further // logical failures are "programs" with various exit messages env, ok := func() ([]string, bool) { mux.Lock() defer mux.Unlock() if executing { return nil, false } executing = true var env []string for k, v := range envValues { env = append(env, k + "=" + v) } return env, ok }() if !ok { fmt.Fprintf(channel.Stderr(), "ERR: invalid environment\r\n") exit(-1) break } re := regexp.MustCompile(`^(git-upload-pack|git-receive-pack) '([^']*)'$`) smatchs := re.FindStringSubmatch(command) if len(smatchs) != 1+2 { fmt.Fprintf(channel.Stderr(), "ERR: invalid SSH 'exec' sent\r\n") exit(-1) break } var dataflow int switch smatchs[1] { case "git-upload-pack": dataflow = DATAFLOW_GIT_UPLOAD_PACK case "git-receive-pack": dataflow = DATAFLOW_GIT_RECEIVE_PACK } repo, ok := ParseGitRepositoryID(smatchs[2]) if !ok { fmt.Fprintf(channel.Stderr(), "ERR: syntactically invalid repository\r\n") fmt.Fprintf(channel.Stderr(), "NOTE: valid repository identifiers are of the form \"~user/repo.git\"\r\n") exit(-1) break } authenticated := auth.HasReadWriteAccess(pubkeyfp, repo) if !authenticated { fmt.Fprintf(channel.Stderr(), "ERR: unauthorized\r\n") exit(-1) break } /*** AUTHENTICATED ***/ cmd := executeAuthenticatedGit( ctx, srv.RepositoriesRoot, io.Reader(channel), io.Writer(channel), io.Writer(channel.Stderr()), env, repo, dataflow, ) if cmd == nil { exit(-1) break } go func() { cmd.Wait() exit(cmd.ProcessState.ExitCode()) }() default: reply(req, false) } case code := <-exited: SendExitStatus(channel, code) return nil case <-ctx.Done(): return ctx.Err() } } } func executeAuthenticatedGit( ctx context.Context, repositoriesRoot string, stdin io.Reader, stdout, stderr io.Writer, env []string, repo *GitRepository, dataflow int, ) (cmd *exec.Cmd) { // NOTE: no locking is done, since git supports multiple concurrent executions of itself ctx, _ = context.WithTimeout(ctx, GIT_MAXIMUM_TRANSFER_TIME) // TODO consider: env = append(env, "HOME=/home/git") env = append(env, "PATH=") sort.Strings(env) path := repo.Path(repositoriesRoot) info, err := os.Lstat(path) if err != nil || !info.IsDir() { log.Printf("SEVERE: repository does not exist: %s", repo.ID()) fmt.Fprintf(stderr, "ERR: the repository %s does not exist on server disk; please contact an administrator\r\n", repo.ID()) fmt.Fprintf(stderr, "NOTE: I realize that I am both the only administrator and likely the only user\r\n") return } switch dataflow { case DATAFLOW_GIT_UPLOAD_PACK: cmd = exec.CommandContext(ctx, "/usr/bin/git-upload-pack", path) case DATAFLOW_GIT_RECEIVE_PACK: cmd = exec.CommandContext(ctx, "/usr/bin/git-receive-pack", path) default: fmt.Fprintf(stderr, "ERR: (internal)\r\n") return } cmd.Env = env cmd.Dir = path cmd.Stdin = stdin cmd.Stdout = stdout cmd.Stderr = stderr err = cmd.Start() if err != nil { log.Printf("could not start: %v", err) fmt.Fprintf(stderr, "ERR: (internal)\r\n") return } return cmd }