### svnserve,一个自定义的服务器 **svnserve**是一个轻型的服务器,可以同客户端通过在TCP/IP基础上的自定义有状态协议通讯,客户端通过使用开头为`svn://`或者`svn+ssh://`**svnserve**的URL来访问一个**svnserve**服务器。这一小节将会解释运行**svnserve**的不同方式,客户端怎样实现服务器的认证,怎样配置版本库恰当的访问控制。 ### 调用服务器 有许多调用**svnserve**的方式,如果调用时没有参数,你只会看到一些帮助信息,然而,如果你计划使用**inetd**启动进程,你可以传递`-i`(`--inetd`)选项: ~~~ $ svnserve -i ( success ( 1 2 ( ANONYMOUS ) ( edit-pipeline ) ) ) ~~~ 当用参数`--inetd`调用时,**svnserve**会尝试使用自定义协议通过*stdin*和*stdout*来与Subversion客户端通话,这是使用**inetd**工作的标准方式,IANA为Subversion协议保留3690端口,所以在类Unix系统你可以在`/etc/services`添加如下的几行(如果他们还不存在): ~~~ svn 3690/tcp # Subversion svn 3690/udp # Subversion ~~~ 如果系统是使用经典的类Unix的**inetd**守护进程,你可以在`/etc/inetd.conf`添加这几行: ~~~ svn stream tcp nowait svnowner /usr/bin/svnserve svnserve -i ~~~ 确定“svnowner”用户拥有访问版本库的适当权限,现在如果一个客户连接来到你的服务器的端口3690,**inetd**会产生一个**svnserve**进程来做服务。 在一个Windows系统,有第三方工具可以将**svnserve**作为服务运行,请看Subversion的网站的工具列表。 **svnserve**的第二个选项是作为独立“守护”进程,为此要使用`-d`选项: ~~~ $ svnserve -d $ # svnserve is now running, listening on port 3690 ~~~ 当以守护模式运行**svnserve**时,你可以使用`--listen-port=`和`--listen-host=`选项来自定义“绑定”的端口和主机名。 也一直有第三种方式,使用`-t`选项的“管道模式”,这个模式假定一个分布式服务程序如**RSH**或**SSH**已经验证了一个用户,并且*以这个用户*调用了一个私有**svnserve**进程,**svnserve**运作如常(通过*stdin*和*stdout*通讯),并且可以设想通讯是自动转向到一种通道传递回客户端,当**svnserve**被这样的通道代理调用,确定认证用户对版本数据库有完全的读写权限,(见[服务器和访问许可:一个警告]( "服务器和访问许可:一个警告")。)这与本地用户通过`file:///`URl访问版本库同样重要。 **服务器和访问许可:一个警告** 首先需要记住,一个Subversion版本库是一组数据库文件,任何进程直接访问版本库需要对整个版本库有正确的读写许可,如果你不仔细处理,这会变得很头痛,特别是当你使用Berkeley DB数据库而不是FSFS时,详细信息可以阅读[“支持多种版本库访问方法”一节]( "支持多种版本库访问方法")。 第二点,当配置**svnserve**、Apache **httpd**或者其它任何服务器时,不要使用`root`用户(或者其它具备无限制权限的用户)启动服务器进程,根据所有权和版本库允许的权限,通常应该创建一个新的自定义用户,例如很多管理员会创建一个叫做`svn`的用户,赋予这个用户排他的拥有权和对Subversion版本库的导出权利,只让服务器以这个用户运行。 一旦**svnserve**已经运行,它会将你系统中所有版本库发布到网络,一个客户端需要指定版本库在URL中的*绝对*路径,举个例子,如果一个版本库是位于`/usr/local/repositories/project1`,则一个客户端可以使用`svn://host.example.com/usr/local/repositories/project1 `来进行访问,为了提高安全性,你可以使用**svnserve**的`-r`选项,这样会限制只输出指定路径下的版本库: ~~~ $ svnserve -d -r /usr/local/repositories … ~~~ 使用`-r`可以有效地改变文件系统的根位置,客户端可以使用去掉前半部分的路径,留下的要短一些的(更加有提示性)URL: ~~~ $ svn checkout svn://host.example.com/project1 … ~~~ ### 内置的认证和授权 如果一个客户端连接到**svnserve**进程,如下事情会发生: - 客户端选择特定的版本库。 - 服务器处理版本库的`conf/svnserve.conf`文件,并且执行里面定义的所有认证和授权政策。 - 依赖于位置和授权政策, - 如果没有收到认证请求,客户端可能被允许匿名访问,或者 - 客户端收到认证请求,或者 - 如果操作在“通道模式”,客户端会宣布自己已经在外部得到认证。 在撰写本文时,服务器还只知道怎样发送CRAM-MD5认证请求,本质上讲,就是服务器发送一些数据到客户端,客户端使用MD5哈希算法创建这些数据组合密码的指纹,然后返回指纹,服务器执行同样的计算并且来计算结果的一致性,*真正的密码并没有在互联网上传递。* 当然也有可能,如果客户端在外部通过通道代理认证,如**SSH**,在那种情况下,服务器简单的检验作为那个用户的运行,然后使用它作为认证用户名,更多信息请看[“SSH认证和授权”一节]( "SSH认证和授权")。 像你已经猜测到的,版本库的`svnserve.conf`文件是控制认证和授权政策的中央机构,这文件与其它配置文件格式相同(见[“运行配置区”一节]("运行配置区")):小节名称使用方括号标记(`[`和`]`),注释以井号(`#`)开始,每一小节都有一些参数可以设置(`variable = value`),让我们浏览这个文件并且学习怎样使用它们。 #### 创建一个用户文件和域 此时,`svnserve.conf`文件的`[general]`部分包括所有你需要的变量,开始先定义一个保存用户名和密码的文件和一个认证域: ~~~ [general] password-db = userfile realm = example realm ~~~ `realm`是你定义的名称,这告诉客户端连接的“认证命名空间”,Subversion会在认证提示里显示,并且作为凭证缓存(见[“客户端凭证缓存”一节]( "客户端凭证缓存")。)的关键字(还有服务器的主机名和端口),`password-db`参数指出了保存用户和密码列表文件,这个文件使用同样熟悉的格式,举个例子: ~~~ [users] harry = foopassword sally = barpassword ~~~ `password-db`的值可以是用户文件的绝对或相对路径,对许多管理员来说,把文件保存在版本库`conf/`下的`svnserve.conf`旁边是一个简单的方法。另一方面,可能你的多个版本库使用同一个用户文件,此时,这个文件应该在更公开的地方,版本库分享用户文件时必须配置为相同的域,因为用户列表本质上定义了一个认证域,无论这个文件在哪里,必须设置好文件的读写权限,如果你知道运行**svnserve**的用户,限定这个用户对这个文件有读权限是必须的。 #### 设置访问控制 `svnserve.conf`有两个或多个参数需要设置:它们确定未认证(匿名)和认证用户可以做的事情,参数`anon-access`和`auth-access`可以设置为`none`、`read`或者`write`,设置为`none`会限制所有方式的访问,`read`允许只读访问,而`write`允许对版本库完全的读/写权限: ~~~ [general] password-db = userfile realm = example realm # anonymous users can only read the repository anon-access = read # authenticated users can both read and write auth-access = write ~~~ 实例中的设置实际上是参数的缺省值,你一定不要忘了设置它们,如果你希望更保守一点,你可以完全封锁匿名访问: ~~~ [general] password-db = userfile realm = example realm # anonymous users aren't allowed anon-access = none # authenticated users can both read and write auth-access = write ~~~ 注意**svnserve**只能识别“整体”的访问控制,一个用户可以有全体的读/写权限,或者只读权限,或没有访问权限,没有对版本库具体路径访问的细节控制,很多项目和站点,这种 访问控制已经完全足够了,然而,如果你希望单个目录访问控制,你会需要使用包括**mod_authz_svn**(见[“钩子脚本”一节]( "钩子脚本"))的Apache,或者是使用**pre-commit**钩子脚本来控制写访问(见[“钩子脚本”一节]( "钩子脚本")),Subversion的分发版本包含一个**commit-access-control.pl**和一个更加复杂的**svnperms.py**脚本可以作为pre-commit脚本使用。 ### SSH认证和授权 **svnserve**的内置认证会非常容易得到,因为它避免了创建真实的系统帐号,另一方面,一些管理员已经创建好了SSH认证框架,在这种情况下,所有的项目用户已经拥有了系统帐号和有能力“SSH到”服务器。 SSH与**svnserve**结合很简单,客户端只需要使用`svn+ssh://`的URL模式来连接: ~~~ $ whoami harry $ svn list svn+ssh://host.example.com/repos/project harry@host.example.com's password: ***** foo bar baz … ~~~ 在这个例子里,Subversion客户端会调用一个**ssh**进程,连接到`host.example.com`,使用用户`harry`认证,然后会有一个**svnserve**私有进程以用户`harry`运行。**svnserve**是以管道模式调用的(`-t`),它的网络协议是通过**ssh**“封装的”,被管道代理的**svnserve**会知道程序是以用户`harry`运行的,如果客户执行一个提交,认证的用户名会作为版本的参数保存到新的修订本。 这里要理解的最重要的事情是Subversion客户端*不*是连接到运行中的**svnserve**守护进程,这种访问方法不需要一个运行的守护进程,也不需要在必要时唤醒一个,它依赖于**ssh**来发起一个**svnserve**进程,然后网络断开后终止进程。 当使用`svn+ssh://`的URL访问版本库时,记住是**ssh**提示请求认证,而*不*是**svn**客户端程序。这意味着密码不会有自动缓存(见[“客户端凭证缓存”一节]( "客户端凭证缓存")),Subversion客户端通常会建立多个版本库的连接,但用户通常会因为密码缓存特性而没有注意到这一点,当使用`svn+ssh://`的URL时,用户会为**ssh**在每次建立连接时重复的询问密码感到讨厌,解决方案是用一个独立的SSH密码缓存工具,像类Unix系统的**ssh-agent**或者是Windows下的**pageant**。 当在一个管道上运行时,认证通常是基于操作系统对版本库数据库文件的访问控制,这同Harry直接通过`file:///`的URL直接访问版本库非常类似,如果有多个系统用户要直接访问版本库,你会希望将他们放到一个常见的组里,你应该小心的使用umasks。(确定要阅读[“支持多种版本库访问方法”一节]( "支持多种版本库访问方法"))但是即使是在管道模式时,文件`svnserve.conf`还是可以阻止用户访问,如`auth-access = read`或者`auth-access = none`。 你会认为SSH管道的故事该结束了,但还不是,Subversion允许你在运行配置文件`config`(见[“运行配置区”一节]( "运行配置区"))创建一个自定义的管道行为方式,举个例子,假定你希望使用RSH而不是SSH,在`config`文件的`[tunnels]`部分作如下定义: ~~~ [tunnels] rsh = rsh ~~~ 现在你可以通过指定与定义匹配的URL模式来使用新的管道定义:`svn+rsh://host/path`。当使用新的URL模式时,Subversion客户端实际上会在后台运行**rsh host svnserve -t**这个命令,如果你在URL中包括一个用户名(例如,`svn+rsh://username@host/path`),客户端也会在自己的命令中包含这部分(**rsh username@host svnserve -t**),但是你可以定义比这个更加智能的新的管道模式: ~~~ [tunnels] joessh = $JOESSH /opt/alternate/ssh -p 29934 ~~~ 这个例子里论证了一些事情,首先,它展现了如何让Subversion客户端启动一个特定的管道程序(这个在`/opt/alternate/ssh`),在这个例子里,使用`svn+joessh://`的URL会以`-p 29934`参数调用特定的SSH程序―对连接到非标准端口的程序非常有用。 第二点,它展示了怎样定义一个自定义的环境变量来覆盖管道程序中的名字,设置`SVN_SSH`环境变量是覆盖缺省的SSH管道的一种简便方法,但是如果你需要为多个服务器做出多个不同的覆盖,或许每一个都联系不同的端口或传递不同的SSH选项,你可以使用本例论述的机制。现在如果我们设置`JOESSH`环境变量,它的值会覆盖管道中的变量值―会执行**$JOESSH**而不是**/opt/alternate/ssh -p 29934**。 ### SSH配置技巧 不仅仅是可以控制客户端调用**ssh**方式,也可以控制服务器中的**sshd**的行为方式,在本小节,我们会展示怎样控制**sshd**执行**svnserve**,包括如何让多个用户分享同一个系统帐户。 #### 初始设置 作为开始,定位到你启动**svnserve**的帐号的主目录,确定这个账户已经安装了一套SSH公开/私有密钥对,用户可以通过公开密钥认证,因为所有如下的技巧围绕着使用SSH`authorized_keys`文件,密码认证在这里不会工作。 如果这个文件还不存在,创建一个`authorized_keys`文件(在UNIX下通常是`~/.ssh/authorized_keys`),这个文件的每一行描述了一个允许连接的公钥,这些行通常是下面的形式: ~~~ ssh-dsa AAAABtce9euch.... user@example.com ~~~ 第一个字段描述了密钥的类型,第二个字段是未加密的密钥本身,第三个字段是注释。然而,这是一个很少人知道的事实,可以使用一个`command`来处理整行: ~~~ command="program" ssh-dsa AAAABtce9euch.... user@example.com ~~~ 当`command`字段设置后,SSH守护进程运行命名的程序而不是通常Subversion客户端询问的**svnserve -t**。这为实施许多服务器端技巧开启了大门,在下面的例子里,我们简写了文件的这些行: ~~~ command="program" TYPE KEY COMMENT ~~~ #### 控制调用的命令 因为我们可以指定服务器端执行的命令,我们很容易来选择运行一个特定的**svnserve**程序来并且传递给它额外的参数: ~~~ command="/path/to/svnserve -t -r /virtual/root" TYPE KEY COMMENT ~~~ 在这个例子里,`/path/to/svnserve`也许会是一个**svnserve**程序的包裹脚本,会来设置umask(见[“支持多种版本库访问方法”一节]( "支持多种版本库访问方法"))。它也展示了怎样在虚拟根目录定位一个**svnserve**,就像我们经常在使用守护进程模式下运行**svnserve**一样。这样做不仅可以把访问限制在系统的一部分,也可以使用户不需要在`svn+ssh://`URL里输入绝对路径。 多个用户也可以共享同一个帐号,作为为每个用户创建系统帐户的替代,我们创建一个公开/私有密钥对,然后在`authorized_users`文件里放置各自的公钥,一个用户一行,使用`--tunnel-user`选项: ~~~ command="svnserve -t --tunnel-user=harry" TYPE1 KEY1 harry@example.com command="svnserve -t --tunnel-user=sally" TYPE2 KEY2 sally@example.com ~~~ 这个例子允许Harry和Sally通过公钥认证连接同一个的账户,每个人自定义的命令将会执行。`--tunnel-user`选项告诉**svnserve -t**命令采用命名的参数作为经过认证的用户,如果没有`--tunnel-user`,所有的提交会作为共享的系统帐户提交。 最后要小心:设定通过公钥共享账户进行用户访问时还会允许其它形式的SSH访问,即使你设置了`authorized_keys`的`command`值,举个例子,用户仍然可以通过SSH得到shell访问,或者是通过服务器执行X11或者是端口转发。为了给用户尽可能少的访问权限,你或许希望在`command`命令之后指定一些限制选项: ~~~ command="svnserve -t --tunnel-user=harry",no-port-forwarding,\ no-agent-forwarding,no-X11-forwarding,no-pty \ TYPE1 KEY1 harry@example.com ~~~ 见RFC 2195。