zoukankan      html  css  js  c++  java
  • GitLab Shell如何通过SSH工作

    转自:https://wayjam.me/post/how-gitlab-shell-works-with-ssh.md

    GitLab访问Git仓库

    首先回顾GitLab的Git仓库四种访问方式:

    1. git pull over http -> gitlab-rails (Authorization) -> accept or decline -> execute git command
    2. git push over http -> gitlab-rails (git command is not executed yet) -> execute git command -> gitlab-shell pre-receive hook -> API call to gitlab-rails (authorization) -> accept or decline push
    3. git pull over ssh -> gitlab-shell -> API call to gitlab-rails (Authorization) -> accept or decline -> execute git command
    4. git push over ssh -> gitlab-shell (git command is not executed yet) -> execute git command -> gitlab-shell pre-receive hook -> API call to gitlab-rails (authorization) -> accept or decline push

    四种方式都有GitLab Shell的参与,但不同过程GitLab Shell发挥了不同的作用,并且它并不是一个整体的服务,而是由一些子命令组合而成。HTTP方式的Git操作,经gitlab workhorse直接交由Rails应用处理,然后通过HTTP协议交换数据,对于git的操作有三条路径:Gem包Rugged、Raw Git命令或者Gitaly,push/pull一般只跟后两种有关,GitLab Shell充当的作用仅仅是git hook的作用。

    SSH方式的push/pull是GitLab Shell的主场景,而Rails在这其中充当了权限再验证的角色。

    Git SSH 传输协议

    首先,简要说明Git是如何通过SSH协议与服务端的Git交互数据。

    ssh git@example.com "the-command"
    

    在客户端执行这样的命令时候,服务端SSHD验证身份通过后,默认将启动一个Shell解析执行the-command的命令。普通使用中,大多都不加自定义命令,这将启动一个Shell交互式命令解析器。

    要了解GitLab Shell的原理,不能不说它的“前任”——Gitolite。Gitolite是一个Git的授权层前端,同样提供了HTTP(httpd)和SSH的方式访问Git仓库。而Gitlab在v5.0完全用GitLab Shell替代了Gitolite,前者完全依赖Rails层的权限认证(项目、分支、用户等的权限),后者则由于需要完全保持一份冗余数据在自身的配置文件,主要由于速度和数据不同步被Gitlab官方放弃。但是,二者仍然采用了authorized_keyscommand magic方案。

    GitLab CE 10.4之后加入了`AuthorizedKeysCommand`的使用(_require_ OpenSSH 6.9+),使用自定义程序匹配Key而不是文件文本匹配。

    SSH command magic

    SSHD服务端收到客户端的连接请求后,会在authorized_keys进行匹配,认证失败则拒绝连接。查看~/.ssh/authorized_keys文件内容如下:

    # Gitolite
    command="[path]/gitolite-shell user-one",[more options] ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArXtCT...
    # GitLab Shell
    command="/home/git/gitlab-shell/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArXtCT...

    可以发现保存的每条公钥前面都有command=,Gitolite和GitLab Shell后面的参数代表着用户的识别信息,Gitolite保存在配置文件,而gitlab保存在数据库。当用户的服务端校验成功后,则会执行command=后的命令,而不是shell access,同时将SSH命令所要执行的命令赋值给SSH_ORIGINAL_COMMAND。所以与客户端交互的真正命令是(以pull代码为例):

    SSH_ORIGINAL_COMMAND=git-upload-pack git-pack/home/git/gitlab-shell/bin/gitlab-shell key-1
    

    "Shell"

    GitLab Shell由ruby脚本和go程序组成,首先看GitLab Shell入口代码:bin/gitlab-shell

    #!/usr/bin/env ruby
    # ...
    key_id = /key-[0-9]+/.match(ARGV.join).to_s
    original_cmd = ENV.delete('SSH_ORIGINAL_COMMAND')
    # ...
    require File.join(ROOT_PATH, 'lib', 'gitlab_shell')
    if GitlabShell.new(key_id).exec(original_cmd)
    # ...
    

    这段代码里面的两个变量:

    • key_id -> sshd调用GitLab Shell时传入的参数。
    • original_cmd -> 即上文提到的SSH_ORIGINAL_COMMAND环境变量,并且获取完即移除。

    举个例子,如果用户通过git客户端调用git clone git@server的时候,实际上git客户端启动的是receive-pack(git由许许多多子命令组成)并且在内部执行的是ssh git@git@server git-upload-pack git@server,那么此时,服务端给GitLab Shell设定的环境变量则是git-upload-pack git@server,。

    然后看lib/gitlab-shell.rb代码,初始化一个GitlabShell,并且执行exec,而exec里面有几个关键步骤:

    def exec(origin_cmd)
      unless origin_cmd
        puts "Welcome to GitLab, #{username}!"
        return true
      end
    
      args = Shellwords.shellwords(origin_cmd)
      args = parse_cmd(args)
    
      if GIT_COMMANDS.include?(args.first)
        GitlabMetrics.measure('verify-access') { verify_access }
      end
    
      process_cmd(args)
    
      true
    rescue something #异常
    end
    
    1. 解析命令 parse_cmd

    主要是处理Windows/Linux命令差异、屏蔽非法命令、LFS命令。

    2. 验证权限 verify_access
    /api/v4/internal/allowed

    通过Rails的HTTP API :/api/v4/internal/allowed发送查询参数到Rails,接口返回此用户对这个仓库是否有此操作的权限。

    参数:

    {
      command => git命令
      repo => 仓库信息
      key_id => SSH key id(在数据库能找查找到对应的用户)
      protocol => ssh/env
    }
    
    3. 处理命令 process_cmd

    处理命令,检测gitaly此特性是否有开启,如果开启则调用gitaly处理,否则则调用git原生命令。

    设定环境变量,然后使用Kernel.exec调用目标进程替代当前进程。

    env = {
        'HOME' => ENV['HOME'],
        'PATH' => ENV['PATH'],
        'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
        'LANG' => ENV['LANG'],
        'GL_ID' => @key_id,
        'GL_PROTOCOL' => GL_PROTOCOL,
        'GL_REPOSITORY' => @gl_repository,
        'GL_USERNAME' => @username
    }
    # ...
    Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH)
    

    处理Git命令

    以Pull,且调用Gitaly为例

    // bin/gitaly-upload-pack
    code, err := handler.UploadPack(os.Args[1], &request)
    // go/internal/handler/upload_pack.go
    func UploadPack(gitalyAddress string, request *pb.SSHUploadPackRequest) (int32, error) {
        # ...
    
        conn, err := client.Dial(gitalyAddress, dialOpts())
        if err != nil {
            return 0, err
        }
        defer conn.Close()
    
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
        return client.UploadPack(ctx, conn, os.Stdin, os.Stdout, os.Stderr, request)
    }
    // go/vendor/gitlab.com/gitlab-org/gitaly/client/upload_pack.go
    func UploadPack(ctx context.Context, conn *grpc.ClientConn, stdin io.Reader, stdout, stderr io.Writer, req *pb.SSHUploadPackRequest) (int32, error) {
        ctx2, cancel := context.WithCancel(ctx)
        defer cancel()
    
        ssh := pb.NewSSHServiceClient(conn)
        stream, err := ssh.SSHUploadPack(ctx2)
        if err != nil {
            return 0, err
        }
    
        if err = stream.Send(req); err != nil {
            return 0, err
        }
    
        inWriter := streamio.NewWriter(func(p []byte) error {
            return stream.Send(&pb.SSHUploadPackRequest{Stdin: p})
        })
    
        return streamHandler(func() (stdoutStderrResponse, error) {
            return smHandler(func() (stdoutStderrResponse, error) {
            return stream.Recv()
        }, func(errC chan error) {
            _, errRecv := io.Copy(inWriter, stdin)
            stream.CloseSend()
            errC <- errRecv
        }, stdout, stderr)
    }
    

    可以看到通过grpc跟gitaly server通信,获得响应之后:

    # go/vendor/gitlab.com/gitlab-org/gitaly/client/std_stream.go
    exited code => resp.GetExitStatus().GetValue()
    stderr => stderr.Write(resp.GetStderr())
    stdout => stdout.Write(resp.GetStdout())

    git客户端将标准错误打印到控制台,解析标准输出作为git数据:通过远程ssh调用命令将数据打印到标准输出传输到客户端解析

    Cloning into 'gitlab-shell'...
    remote: Counting objects: 5558, done.
    remote: Compressing objects: 100% (2548/2548), done.
    remote: Total 5558 (delta 3051), reused 5117 (delta 2754)
    Receiving objects: 100% (5558/5558), 2.83 MiB | 2.72 MiB/s, done.
    Resolving deltas: 100% (3051/3051), done.

    Refs

  • 相关阅读:
    索引在什么情况下遵循最左前缀的规则?
    MySQL索引种类
    简述触发器、函数、视图、存储过程?
    6.Class 与 Style 绑定
    2.Javascript 函数(主要)
    Java的string类
    PHP+mysql注入的基本过程
    Android自动化测试Emmagee
    EclEmma的介绍、安装与使用
    软件测试方法
  • 原文地址:https://www.cnblogs.com/rongfengliang/p/10447555.html
Copyright © 2011-2022 走看看