Mercurial Server中集成git
我们团队使用 Mercurial 管理程序代码,并使用 mercurial-server 来实现SSH访问和权限管理。
我们有个项目运行在 LeanCloud,而 LeanCloud 只支持 Git 部署代码和 SSH 协议。这样就使得我们必须支持 Git 来管理这个项目的代码,同时意味着参与项目的成员权限管理也要针对 Git 重新弄一套。
mercurial-server 实现权限管理的原理是解析 SSH_ORINGIAL_COMMAND
,并在执 SSH_ORIGINAL_COMMAND
之前(针对分支和文件的权限验证是在pretxnchangegroup
hook 中进行的)进行权限验证的。
既然 mercurial-server 在执行 SSH_ORINGIAL_COMMAND
之前做的权限验证(暂时忽略针对文件和分支的权限验证),那么用它来管理 Git 代码库的权限理论上也是可以的。
来看看 mercruial-server 的源码结构:
├── setup.py
└── src
├── hg-ssh
├── init/
├── mercurialserver/
└── refresh-auth
源码中的 hg-ssh 文件是关键
#!/usr/bin/env python
...
import sys, os, os.path
import base64
import re
from mercurialserver import config, ruleset
...
### 此处有省略
def getrepo(op, repo):
# First canonicalise, then check the string, then the rules
repo = repo.strip().rstrip("/")
if len(repo) == 0:
fail("path to repository seems to be empty")
if repo.startswith("/"):
fail("absolute paths are not supported")
checkDots(repo)
ruleset.rules.set(repo=repo)
if not ruleset.rules.allow(op, branch=None, file=None):
fail("access denied")
return repo
cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
if cmd is None:
fail("direct logins on the hg account prohibited")
elif cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'):
repo = getrepo("read", cmd[6:-14])
if not os.path.isdir(repo + "/.hg"):
fail("no such repository %s" % repo)
dispatch.dispatch(request(['-R', repo, 'serve', '--stdio']))
elif cmd.startswith('hg init '):
repo = getrepo("init", cmd[8:])
if os.path.exists(repo):
fail("%s exists" % repo)
d = os.path.dirname(repo)
if d != "" and not os.path.isdir(d):
os.makedirs(d)
dispatch.dispatch(request(['init', repo]))
else:
fail("illegal command %r" % cmd)
从源码中我们可以看到,hg-ssh 获得了 SSH_ORIGINAL_COMMAND
并检测了是否是 Mercurial 相关命令请求,如果是则解析出请求的目标代码库,如果不是则响应该命令非法。确认是 Mercurial 相关命令以后,则使用 getrepo
函数进行处理并验证了权限。
OK, 我们清楚 mercurial-server 的工作流程以后就可以思考怎样把 Git 集成进去。
首先,我们需要搞清楚,Git 自身是怎样处理 SSH 协议请求的: Git-Internals-Transfer-Protocols
其次,我们知道在独立搭建 Git 服务端的时候,通常建立单独的 git
用户来管理,而且专门为 git
用户配置独立的shell,叫做 git-shell
。从文档中我们了解到 git-shell
会处理的命令只有以下几类:
git receive-pack <argument>
git upload-pack <argument>
git upload-archive <argument>
cvs server
cvs server
这条我们暂时忽略,大多数情况下用不上。
如此,我们就清楚 Git 客户端所有请求都会通过上面的几类命令实现提交和请求。那么,是时候动手集成到 mercurial-server 中了。
修改 hg-ssh 文件
...
elif cmd.startswith('git'):
...
增加elif
区间,判断 cmd
是否是 git
开头的命令。我们所有 Git 请求的处理都在这个区间中进行。
上面我们清楚 Git 只会有上面几类命令,所以为了安全,我们也只应答上面几类命令。所以增加两个变量,另外需要从 SSH_ORINGIAL_COMMAND
里面解析出代码库的路径和验证它是否安全,所以还需要一个正则表达式:
allowedGitReadCommands = [
'git-upload-pack',
'git upload-pack',
'git-upload-archive',
'git upload-archive'
]
allowedGitWriteCommands = [
'git-receive-pack',
'git receive-pack',
]
allowedGitRe = re.compile("^'/*(?P<path>[a-zA-Z0-9][a-zA-Z0-9@._-]*(/[a-zA-Z0-9][a-zA-Z0-9@._-]*)*)'$")
接下来就是处理 Git 请求的具体逻辑了
...
elif cmd.startswith('git'):
try:
verb, args = cmd.split(None, 1)
except ValueError:
fail("Illegal command %r" % cmd)
if verb == 'git':
try:
subverb, args = args.split(None, 1)
except ValueError:
fail("Illegal command %r" % cmd)
verb = '%s %s' % (verb, subverb)
if (verb not in allowedGitWriteCommands and verb not in allowedGitReadCommands):
fail("Illegal command %r" % cmd)
match = allowedGitRe.match(args)
if match is None:
fail("Unsafe command arguments.")
path = match.group('path')
repo = getrepo("read", path)
newCmd = "%(verb)s '%(path)s'" % dict(
verb=verb,
path=os.path.join(config.getReposPath(), path))
os.execvp('git', ['git','shell','-c',newCmd])
...
同样使用 mercurial-server 的 getrepo
来验证权限,因为 mercurial-server 的权限验证只关心请求的代码库路径和当前请求的 SSH Public Key。
这样我们就简单的实现了通过 mercurial-server 来管理 Git 代码库的权限,之后在此基础上增加对 Git 代码库中分支和文件等更细的权限控制。