# 转自 https://forcemz.net/git/2019/03/16/MakeAGitSSHServer/
package main
import (
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/gliderlabs/ssh"
)
// Error
var (
ErrPublicKeyNotFound = errors.New("Public key not found")
ErrEncodeHandshake = errors.New("Handshake encode error")
ErrRepositoryNotFound = errors.New("Repository Not Found")
ErrUnreachablePath = errors.New("Path is unreachable")
)
// StrSplitSkipEmpty skip empty string suggestcap is suggest cap
func StrSplitSkipEmpty(s string, sep byte, suggestcap int) []string {
sv := make([]string, 0, suggestcap)
var first, i int
for ; i < len(s); i++ {
if s[i] != sep {
continue
}
if first != i {
sv = append(sv, s[first:i])
}
first = i + 1
}
if first < len(s) {
sv = append(sv, s[first:])
}
return sv
}
// RepoPathClean todo
func RepoPathClean(p string) (string, error) {
xp := path.Clean(p)
pv := StrSplitSkipEmpty(xp, '/', 4)
if len(pv) != 2 || len(pv[0]) < 2 {
return "", ErrUnreachablePath
}
return pv[0] + "/" + strings.TrimSuffix(pv[1], ".git"), nil
}
// GetSessionEnv env
func GetSessionEnv(s ssh.Session, key string) string {
prefix := key + "="
kl := len(prefix)
for _, str := range s.Environ() {
if kl >= len(str) {
continue
}
if strings.HasPrefix(str, prefix) {
return str[kl:]
}
}
return ""
}
// RepoPathStat stat repo
func RepoPathStat(repo string) (string, error) {
r := filepath.Join("/home/git/root", repo[0:2], repo)
if !strings.HasSuffix(r, ".git") {
r += ".git"
}
if _, err := os.Stat(r); err != nil {
return "", ErrRepositoryNotFound
}
return r, nil
}
// GitCommand exe git-*
func GitCommand(s ssh.Session, scmd string, repo string) int {
if s.User() != "git" {
// filter unallowed user
fmt.Fprintf(s.Stderr(), "Permission denied, user: \x1b[31m'%s'\x1b[0m\n", s.User())
return 127
}
pwn, err := RepoPathClean(repo)
if err != nil {
fmt.Fprintf(s.Stderr(), "Permission denied: \x1b[31m%v\x1b[0m\n", err)
return 127
}
// TODO AUTH
diskrepo, err := RepoPathStat(pwn)
if err != nil {
fmt.Fprintf(s.Stderr(), "Access deined: \x1b[31m%v\x1b[0m\n", err)
return 127
}
version := GetSessionEnv(s, "GIT_PROTOCOL")
cmd := exec.Command("git", scmd, diskrepo)
ProcAttr(cmd)
cmd.Env = append(environ, "GL_ID=key-"+strconv.FormatInt(1111, 10)) /// to set envid
if len(version) > 0 {
cmd.Env = append(environ, "GIT_PROTOCOL="+version) /// to set envid
}
cmd.Env = append(environ, s.Environ()...) /// include other
cmd.Stderr = s.Stderr()
cmd.Stdin = s
cmd.Stdout = s
// FIXME: check timeout
if err = cmd.Start(); err != nil {
fmt.Fprintln(s.Stderr(), "Server internal error, unable to run git-", scmd)
log.Printf("Server internal error, unable to run git-%s error: %v", scmd, err)
return 127
}
var exitcode int
exitChain := make(chan bool, 1)
go func() {
if err = cmd.Wait(); err != nil {
exitcode = 127
}
exitChain <- true
}()
for {
select {
case <-exitChain:
return exitcode
case <-s.Context().Done():
cmd.Process.Kill()
return 0
}
}
}
func sshAuth(ctx ssh.Context, key ssh.PublicKey) bool {
/// TODO auth
return true
}
func sessionHandler(s ssh.Session) {
cmd := s.Command()
if len(cmd) < 2 || !strings.HasPrefix(cmd[0], "git-") {
s.Stderr().Write([]byte("bad command " + cmd[0] + "\n"))
return
}
GitCommand(s, strings.TrimPrefix(cmd[0], "git-"), cmd[1])
}
// Userid get
var Userid uint32
// Groupid in
var Groupid uint32
// NeedSetsid is
var NeedSetsid bool
// ProcAttr is
func ProcAttr(cmd *exec.Cmd) {
if NeedSetsid {
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: Userid,
Gid: Groupid,
},
Setsid: true,
}
}
}
// InitializeUtils ini
func InitializeUtils() error {
if syscall.Getuid() != 0 {
NeedSetsid = false
Userid = uint32(syscall.Getuid())
Groupid = uint32(syscall.Getgid())
return nil
}
//
user, err := user.Lookup("git")
if err != nil {
return err
}
xid, err := strconv.ParseUint(user.Uid, 10, 32)
if err != nil {
return err
}
environ = os.Environ()
for i, s := range environ {
if strings.HasPrefix(s, "HOME=") {
environ[i] = fmt.Sprintf("HOME=%s", user.HomeDir)
}
}
Userid = uint32(xid)
zid, err := strconv.ParseUint(user.Gid, 10, 32)
if err != nil {
return err
}
Groupid = uint32(zid)
NeedSetsid = true
return nil
}
var environ []string
func main() {
srv := &ssh.Server{
Handler: sessionHandler,
PublicKeyHandler: sshAuth,
MaxTimeout: time.Second * 180,
IdleTimeout: time.Second * 3600,
Version: "Basalt-2.0-Single",
}
InitializeUtils()
log.Println("starting ssh server on port 2222...")
ln, err := net.Listen("tcp", ":2222")
if err != nil {
return
}
srv.Serve(ln)
}