昨天看了开源的codefever 以及以前简单学习过gogs,刚才学习下git over ssh 的实现机制
基于openssh + golang (golang 部分参考了gogs 处理)实现了一个简单的git server (ssh 协议的)
原理说明
核心还是我们的openssh server 创建一个git 账户,此账户使用了authorized_keys的forcecommand 功能
forcecommand 中我们添加了git 的处理
- forcecommand 代码
package main
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)
func init() {
file := "./" + "message" + ".txt"
logFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0766)
if err != nil {
panic(err)
}
log.SetOutput(logFile)
log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
}
func parseSSHCmd(cmd string) (string, string) {
ss := strings.SplitN(cmd, " ", 2)
if len(ss) != 2 {
return "", ""
}
return ss[0], strings.Replace(ss[1], "'/", "'", 1)
}
func main() {
// forcecommand 会包含一个SSH_ORIGINAL_COMMAND 里边是git 的一些操作命令,包含了具体命令的处理
println(os.Getenv("SSH_ORIGINAL_COMMAND"))
sshCmd := os.Getenv("SSH_ORIGINAL_COMMAND")
verb, args := parseSSHCmd(sshCmd)
repoFullName := strings.ToLower(strings.Trim(args, "'"))
verbs := strings.Split(verb, " ")
var gitCmd *exec.Cmd
gitRepoPath := fmt.Sprintf("/opt/gitrepo/%s", repoFullName)
os.Setenv("MY_UID", "dalongrong")
if len(verb) == 2 {
println(verbs[0], verbs[1], gitRepoPath)
gitCmd = exec.Command(verbs[0], verbs[1], gitRepoPath)
} else {
println(verbs[0], gitRepoPath)
gitCmd = exec.Command(verb, gitRepoPath)
}
gitCmd.Stdin = os.Stdin
gitCmd.Stdout = os.Stdout
gitCmd.Stderr = os.Stderr
if err := gitCmd.Run(); err != nil {
log.Fatal("Internal error", "Failed to execute git command: %v", err)
}
return
}
docker 构建
FROM golang:1.17-alpine AS build-env
WORKDIR /go/src/app
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn
COPY . .
RUN apk update && apk add git \
&& go build -o git-shell
FROM alpine:latest
WORKDIR /app
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
COPY --from=build-env /go/src/app/git-shell /app/git-shell
- authorized_keys 配置格式
目前比较简单,没有进行复杂的校验处理
command 部分使用了自己开发的git 处理
command="/opt/git-shell",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty <ssh public key>
- openssh 服务启动
基于docker-compose 运行
version: '3'
services:
ssh:
image: dalongrong/openssh-server
build: ./
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- SUDO_ACCESS=true #optional
- PASSWORD_ACCESS=true #optional
- USER_PASSWORD=dalongdemo #optional
- USER_NAME=git #optional
volumes:
- ./config:/config
- ./gitrepo:/opt/gitrepo
ports:
- "2222:2222"
使用说明
- 创建bare git repo
启动之后我们需要创建git repo 注意是bare 模式的
进入openssh 容器
su git
cd /opt/gitrepo
git init --bare demoapp2.git
效果
- 添加ssh 认证客户
config的.ssh 文件夹中的authorized_keys 格式如上 - clone 代码
git clone ssh://git@localhost:2222/demoapp2.git demoapp2
效果
push 代码
cd demoapp2
touch rong.txt
git add --all
git commit -m "add"
git push
效果
说明
如果有权限的问题就可能需要设置先权限了(openssh 容器内执行)
chmod 0755 /opt/gitrepo /config
chmod 700 /config/.ssh
以上是一个简单的学习,参考了gogs、gitlab 等开源项目,基于此刚才对于git ssh server 就有了一个比较完整的了解
完整代码已经放github 了,大家可以参考,实际上目前一些开源的git 支持ssh 协议的基本都是这个套路,只是authorized_keys
的forcecommand 处理上大家好多是不一样的,而且新版本的gitlab 已经自己写了一个ssh server,同时推荐基于AuthorizedKeysCommand
更好的优化处ssh key 的处理了
参考资料
https://github.com/rongfengliang/write-one-git-ssh-server
https://github.com/gogs/gogs
https://github.com/PGYER/codefever
https://git-scm.com/docs/git-receive-pack
https://git-scm.com/docs/git-upload-pack
https://www.ssh.com/academy/ssh/authorized_keys/openssh
https://blog.scalesec.com/just-in-time-ssh-provisioning-7b20d9736a07
https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE
https://git-scm.com/docs/pack-protocol/2.2.3