SVN二次开发
——让SVN、TSVN(TortoiseSVN)支持windows的访问控制模型、NTFS ADS
(可选数据流、NTFS的安全属性)
SVN secondary development:Let svn(subversion),TSVN(TortoiseSVN) support NTFS ADS(NTFS alternate data streams), NTFS Security Properties(NTFS Security attributes,NTFS Extended Attributes),Windows access control model
前两年和北京的一家信息安全公司合作对svn1.6.16进行了二次开发,今天有时间在博客中概要的整理记录一下。
Windows操作系统支持NTFS格式的文件系统,NTFS和FAT相比怎加了附加属性或叫扩展属性(NTFS Extended Attributes),也称为:ADS(Alternate Data Streams, 可选数据流、附加数据流、交换数据流)、NTFS安全属性(NTFS Security Properties,NTFS Security attributes),NTFS是一个有安全性质的文件系统,除了windows操作系统支持的windows访问控制模型(Windows access control model)的安全属性外,客户还可利用ADS实现一些用户专门应用,如设置密级属性页,存放专门的保密数据等。然而,SVN(Subversion)不支持NTFS的扩展属性(安全属性、ADS),这给使用扩展属性(安全属性、ADS)且又使用SVN的用户带来不便,限制了SVN的适用范围。前两年和北京一家信息安全公司合作对svn1.6.16进行了二次开发,使之增加了对NTFS安全属性(即NTFS的扩展属性、NTFS ADS)的支持,大大方便了用户的使用,扩展了SVN的适用范围。
subversion-1.6.16核心源代码26.7M,加上支撑环境源代码,编译后达407M,使之支持NTFS安全属性的二次开发涉及客户端和服务端核心代码,工作量很大,收集的相关资料高达7G,项目的总体设计、详细设计、开发笔记等纸质文档就高达400多页,如果用博客把各个细节记录下来,就要写成多集长篇了,因时间所限只能简单的整理成本文,它包含windows的访问控制模型、SVN的简介、二次开发的总体框图、SVN源代码分析概要。有需要详细资料和支持NTFS扩展属性(NTFS安全属性)的SVN、TSVN网友请和我联系:QQ:1561724180,0311-87024917.
一、windows的访问控制模型 (NTFS的安全属性)
Windows NT的最初设计目标就是提供一个为操作系统提供安全实现的层,为它的对象实现安全保护,其实现方法是通过访问控制模型(Access Control Model)来实现的 。Windows访问控制模型是Windows安全性的基础构件。访问控制模型有两个主要的组成部分,访问令牌(Access Token)和安全描述符(SD:Security Descriptor),它们分别是访问者和被访问者拥有的东西。通过访问令牌和安全描述符的内容,Windows可以确定持有令牌的访问者能否访问持有 安全描述符的对象。
访问令牌是与特定的Windows账户关联的。当一个Windows账户登录的时候,系统会从内部数据库里读取该账户的信息,然后使用这些信息生成一个访问令牌。在该账户环境下启动的进程,都会获得这个令牌的一个副本,进程中的线程默认持有这个令牌。线程要想去访问某个对象,或者执行某些系统管理相关的操作时,Windows就会使用这个线程持有的令牌进行访问检查。
安全描述符是与被访问对象关联的,比如:文件、目录,它包含:
- 版本号。
- 这个对象所有者的OwnerSID和GroupSID(SID:Security IDentifiers)。
- *访问控制列表DACL(Discretionary Access Control List),它包含由对象的所有者定义的对象访问控制项(ACE,Access Control Entry),每个访问控制项的内容描述了允许或拒绝特定账户对这个对象执行特定操作。
- 系统访问控制列表SACL(System Access Control List,有时称为审核 ACE)是一种控制与资源关联的审核消息的机制。与DACL相似,SACL包含定义指定资源的审核规则的ACE。通过审核ACE,可以记录访问资源的成功尝试或失败尝试,但与DACE不同的是,审核ACE不控制哪些账户可以使用某个资源。例如,可以创建一个ACE并将其应用于某个文件的SACL,以记录打开该文件的所有成功尝试。
- 还有其自身的一些控制位。
该结构体在WRK中定义如下:
typedef struct _SECURITY_DESCRIPTOR {
UCHAR Revision;
UCHAR Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control;
PSID Owner;
PSID Group;
PACL Sacl;
PACL Dacl;
} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
下图说明了,安全对象和DACL以及访问者之间的联系(来源于MSDN)。注意,DACL表中的每个ACE的顺序是有意义的,如果前面的Allow(或denied)ACE通过了,那么,系统就不会检查后面的ACE了。
windows的文件、目录的安全描述符SD保存在NTFS文件系统的扩展属性中,对于FAT文件系统因为不支持扩展属性,所以不能进行安全控制。如果你的操作系统是NTFS,那么,你可以看到你创建出来的文件的安全属性的样子。
二、windows安全属性API
文件和目录的的安全属性不能直接操作,但可以通过微软提供的API函数进行读写,这些API函数有:
- AddAccessDeniedAce,加入一个Access-Denied 的ACE。
- DeleteAce,删除一个ACE。
- IsValidAcl,检查你所设置的ACL是否合法。
- IsValidSecurityDescriptor,检查SD的合法性。
- MakeAbsoluteSD和MakeSelfRelativeSD,两个函数可以在两种SD的格式中进行转换。
- SetSecurityDescriptorDacl 和 SetSecurityDescriptorSacl,把ACL设置到SD中。
- 使用GetSecurityDescriptorDacl or GetSecurityDescriptorSacl,取得SD中的ACL结构。
这些和SD/ACL/ACE相关的API函数叫作Low-Level Security Descriptor Functions,其详细信息还请参看MSDN。
关于如何使用这些函数在程序中操作NTFS的安全属性网上例子很多,这里不再赘述,有需要的网友可以联系我:0311-87024917,QQ:1561724180
三、SVN(Subversion)、TortoiseSVN
SVN,即Subversion,是一个*开源的版本控制系统,可以将数据恢复到早期版本,或者检查数据修改的历史,这些数据可以是包括源代码在内的任何其他类型的文件。
Subversion 是一个*/开源的版本控制系统。也就是说,在 Subversion 管理下,文件和目录可以超越时空。也就是 Subversion 允许你数据恢复到早期版本,或者是检查数据修改的历史。正因为如此,许多人将版本控制系统当作一种神奇的“时间机器”。
SVN确实可以像一个时间机器一样,回到任意时刻的版本,查看任意两个时刻的版本变动,不止在协同开发中,即使在个人开发过程中,这种特性都是非常非常有用的,我曾经有过这种经历,对代码进行很多的修改,发现修改的想法根本是错误的,而这时我已经修改了多个文件,要想回退是非常纠结的事情,而现在可以使用SVN轻松做到这一点。
某些版本控制系统本身也是软件配置管理(SCM)系统,这种系统经过精巧的设计,专门用来管理源代码树,并且具备许多与软件开发有关的特性—比如,对编程语言的支持,或者提供程序构建工具。不过 Subversion 并不是这样的系统。它是一个通用系统,可以管理任何类型的文件集。对你来说,这些文件这可能是源程序,而对别人,则可能是一个货物清单或者是数字电影。
SVN总体架构如下图所示,图中的一端是保存所有版本数据的 Subversion 版本库,另一端是Subvesion 的客户程序,管理着所有版本数据的本地影射(称为“工作副本”),在这两极之间是各种各样的版本库访问(RA)层,某些使用电脑网络通过网络服务器访问版本库,某些则绕过网络服务器直接访问版本库。
DAV是Apache HTTP服务器的一个插件,使版本库可以通过网络访问。DAV的意思是“Distributed Authoring and Versioning”。最初的WebDAV标准得到了广泛的成功,所有的现代操作系统拥有内置的(后面有详细资料)对普通WebDAV的支持,许多流行的应用程序(如: Microsoft Office、Dreamweaver和Photoshop)也可以使用WebDAV。在服务器方面,Apache从1998年就开始支持WebDAV,并被认为是一个事实上的开源标准,也有许多商业的WebDAV服务器,例如Microsoft的IIS。
TortoiseSVN是Windows下的GUI客户端,TortoiseSVN 与Windows 外壳(例如资源管理器)无缝集成,安装后右击文件或文件夹即可通过右键菜单使用。
通常我们保存一个文件的不同版本是保存各个本版的本身,SVN不是这样,在版本库中同一个文件的数个连续修订版本以增量式的方式保存,Subversion在保存修订版本时,仅保存与前一个修订版本之间的差异,通过这些差异足以从前一个修订版本中重建当前的修订版本。换句话说,在保存文件中的每一个修订版本仅包含这个修订版本作出的修改。这个规则的唯一一个例外是当前第一个修订版本。
Subversion用一个二进制差异算法描述文件的变化,对于文本(可读)和二进制 (不可读)文件其操作方式是一致的。
Subversion在版本库中只能保存文件和目录的普通属性,而NTFS扩展属性(安全属性)则不支持,这正是我们下面要解决的问题。
Subversion服务端支持Berkeley DB和FSFS,我们的二次开发针对FSFS,网上有个FSFS的英文资料《Subversion FSFS》,因开发需要我们把它翻译成了中文,形成了一个中英文对照版,有需要的可以和我们联系(联系方式见最后)。
三、subversion二次开发总体方案图
在访问层客户端一侧增加NTFS安全属性的读出、设置、发送、接收模块,在客户端执行import、export、checkout、update、commit等命令时,访问层通过新增的模块读出或设置相应文件或文件夹的NTFS的安全属性。
在版本库层增加NTFS安全属性(NTFS附加属性)的发送、接收、保存模块,完成客户端传输过来的相应文件、文件夹的NTFS安全属性的保存工作,或把相应的功能直接嵌入原来的相应的功能模块。
支持NTFS安全属性的SVN二次开发总体方案图:
四、源代码分析:
开发过程中形成了数百页的源代码分析资料,这里只做简单的介绍,展现一个思路,有需要详细资料的网友可和我们联系,QQ:1561724180
1、总体概念
编辑器(Editor): include\svn_delta.h
所谓的编辑器就是一个全是回调函数的结构(struct svn_delta_editor_t),增量源将调用这些函数产生增量。见include\svn_delta.h
编组和解组编辑器操作的通用框架: libsvn_ra_svn\editorp.c
SVN协议的客户端和服务器需要驱动和消费编辑器。对于提交,客户端驱动和服务器消耗;对于更新/交换/状态/差异命令(update/switch/status/diff),服务器驱动和客户端消耗。 editorp.c提供了一个在SVN连接上编组和解组编辑器操作的通用框架;服务器和客户端两端都使用。
树增量的生产者和消费者:
在Subversion中有各种树增量的生产者和消费者。下面举一个提交命令处理的例子:
- 客户端检查其工作副本数据,并产生用于提交的描述变化的树增量。
- 客户端网络库消耗这个增量,并以等效的系列网络请求通过线路发送他们(例如,以ra_svn协议流发送到svnserve,或以WebDAV命令发送到Apache httpd服务器)
- 服务器收到这些请求,并生产树增量(如果顺利的话,等同于上面客户端生产那个)。Subversion服务器模块消费该增量,并向文件系统提交一个相应的事务。
其他的情况详见include\svn_delta.h中的注释。
2、访问层数据流
数据流的构建
svn访问层数据流有两种socket和file,分别用于网络传输和本地文件传输,它们都使用svn_ra_svn__stream_t结构来实现。下面的两个函数用于构建两种数据流。详见libsvn_ra_svn\streams.c。
libsvn_ra_svn\streams.c
svn_ra_svn__stream_t * svn_ra_svn__stream_from_files(
apr_file_t *in_file,
apr_file_t *out_file,
apr_pool_t *pool) svn_ra_svn__stream_t * svn_ra_svn__stream_from_sock(apr_socket_t *sock,
apr_pool_t *pool)
对win socket接口的调用
对于windows下的socket数据流sock_read_cb、sock_write_cb会安照下面的路径最终调用win Socket的recv、send函数从socket套接口接收和发送数据。下面值列出了sock_read_cb的调用路径,sock_write_cb也一样,可参照理解。
sock_read_cb libsvn_ra_svn\streams.c
↓
apr_socket_recv APR\network_io\win32\sendrecv.c。
↓ 注:APR\network_io内有beos、os2、unix、win32四种os
WSARecv or recv win socket接口
网络访问数据流的总体流程
服务端 客户端
svnserve\main.c libsvn_ra_svn\client.c
↓ ↓
+--->--------\/-------<---+
↓
svn_ra_svn_create_conn libsvn_ra_svn\marshal.c
↓
svn_ra_svn__stream_from_sock libsvn_ra_svn\streams.c
↓
svn_ra_svn__stream_create(b, libsvn_ra_svn\streams.c
<---- sock_read_cb, 创建svn_ra_svn__stream_t并
↓<--- sock_write_cb, 使用这里的参数初始化他,并返
↓ sock_timeout_cb, 回他的句柄。
↓ sock_pending_cb, pool)
↓
↓---->----
↓
apr_socket_recv apr\network_io\win32\sendrecv.c
apr_socket_send
↓
recv or WSARecv win socket
send or WSASend
这种机制作的非常灵活,对于文件操作、串口操作、USB读写操作等,只需要在调用svn_ra_svn__stream_create函数时传递相应的读写函数就可以了。
3、Subversion服务端import的处理过程和调用关系
main()主流程
↓
serve(conn, ¶ms, connection_pool) svnserve\serve.c
↓
svn_ra_svn_handle_commands2(......) libsvn_ra_svn\marshal.c
循环处理客户端发过来的各个命令,命令及其处理函数在svnserve\serve.c文件中定义在report_commands[]、main_commands[]数组。见上面。
对于svn import处理的命令有:check_path、commit
↓
(*command->handler)(conn, iterpool, params, baton)
↓ 下面以处理commit为例,即command->cmdname==“commit”
commit() svnserve\serve.c
↓
svn_repos_get_commit_editor5 libsvn_repos\commit.c
↓ 设置:e->add_file=add_file,处理add_file编辑命令使用该函数
svn_ra_svn_drive_editor libsvn_ra_svn\editorp.c
↓
svn_ra_svn_drive_editor2 libsvn_ra_svn\editorp.c
循环处理客户端发过来的各个editor命令,如:open-root、add-file、apply-textdelta、textdelta-chunk、textdelta-end、close-file、close-dir、close-edit
ra_svn_edit_cmds[]赋值初始化定义在editorp.c的后半部分
↓
(*ra_svn_edit_cmds[i].handler)(conn, subpool, params, &state)
↓ 对于"add-file"命令
ra_svn_handle_add_file libsvn_ra_svn\editorp.c
通过客户端传过来的令牌token在ra_svn_driver_state_t *ds中找到操作的版本库。
↓
ds->editor->add_file(path, entry->baton, copy_path, copy_rev, ds->file_pool, &file_entry->baton))
在(libsvn_repos\commit.c的) svn_repos_get_commit_editor5函数中设置:e->add_file=add_file,见上面对svn_repos_get_commit_editor的调用
↓
add_file libsvn_repos\commit.c
(const char *path, void *parent_baton, const char *copy_path,
svn_revnum_t copy_revision, apr_pool_t *pool, void **file_baton)
↓
svn_fs_make_file(eb->txn_root, full_path, subpool) libsvn_fs\fs-loader.c
↓
fs_make_file(svn_fs_root_t *root, libsvn_fs_fs\tree.c
const char *path,
apr_pool_t *pool)
该函数将完成在版本中增加一个文件,但这个文件只有在执行close-edit命令后才会真正出现在版本仓库中。
4、Subversion客户端import的处理过程和调用关系
- 服务端:
版本仓库位置:c:\svn\t t为版本仓库名称
服务端启动参数:F:svnserve -d -r c:\svn
- 客户端:
要导入的文件位置:f:\svn-ntfs\kf\test
客户端要导入的文件内容:
f:\svn-ntfs\kf\test
| 1.txt
\---test_dir
2.txt
启动:C:>svn import f:\svn-ntfs\kf\test svn://192.168.0.3/t/ -m "Test"
注:不要丢掉-m "Test"参数,否则需要指定编辑器,如果不指定,则不能正常运行。网上有资料说,windows下可用gvim.exe或Posix Tools vi.exe。祥见《windows下subversion的编译与调试.doc》。
main() svn\main.c
↓
(*subcommand->cmd_func)(os, &command_baton, pool);
↓该调用在main()函数的最后。各子命令设置在子命令表svn_cl__cmd_table[]
svn_cl__import svn\import-cmd.c
↓在svn_cl__import函数内用命令行参数设置到处路径path,path= ""为当前目录。
svn_client_import3(...,path,...) libsvn_client\commit.c
↓
get_ra_editor libsvn_client\commit.c
↓------------------ 建立网络连接 --------------------------
svn_client__open_ra_session_internal libsvn_client\ra.c
↓ ↓
svn_ra_open3 libsvn_ra\ra_loader.c
↓ ↓
vtable->open_session(......) 在svn_ra_open3()函数内
↓ ↓ 虚函数表vtable定义在libsvn_ra_svn\client.c内
ra_svn_open libsvn_ra_svn\client.c
↓ ↓
open_session libsvn_ra_svn\client.c
↓ ↓
make_connection-->apr_socket_connect-->win Socket的conn()建立连接
↓ ↓ libsvn_ra_svn\client.c
svn_ra_svn_create_conn libsvn_ra_svn\marshal.c
↓----------------------- 网络连接已建立 -------------------
svn_ra_check_path libsvn_ra\ra_loader.c
↓ 给服务端发送“check_path”命令。用于确认版本库URL存在。
↓ ↓
vtable->check_path
↓ ↓ 虚函数表vtable定义在libsvn_ra_svn\client.c内
ra_svn_check_path libsvn_ra_svn\client.c
↓ ↓
svn_ra_svn_write_cmd(conn, pool, "check-path",...)
↓
svn_ra_get_commit_editor3 libsvn_ra\ra_loader.c
↓ 给服务端发送“commit”命令。告诉服务端开始提交。
↓ ↓
session->vtable->get_commit_editor
↓ ↓ 虚函数表vtable定义在libsvn_ra_svn\client.c内
ra_svn_commit libsvn_ra_svn\client.c
↓ ↓
svn_ra_svn_write_tuple(conn,..., "commit",...);
↓ ↓●
svn_ra_svn_get_editor libsvn_ra_svn\editorp.c
↓ 设置编辑器使用的各个函数
↓
import 递归的向版本仓库导入文件或目录 libsvn_client\commit.c
↓
import_dir-->import_file or 递归调用import_dir
or libsvn_client\commit.c
import_file
↓ ↓
editor->add_file
↓ ↓ ↓
在(libsvn_ra_svn\editorp.c的)svn_ra_svn_get_editor
↓ ↓ 函数中设置:ra_svn_editor->add_file = ra_svn_add_file
ra_svn_add_file libsvn_ra_svn\editorp.c
↓ ↓ ↓
svn_ra_svn_write_cmd(b->conn, pool, "add-file", ...)
↓ ↓ libsvn_ra_svn\editorp.c
↓ ↓
send_file_contents(path, file_baton, ...) libsvn_client\commit.c
该函数中使用的svn_stream_t 结构定义在libsvn_subr\stream.c
↓ ↓ ↓
editor->apply_textdelta
↓ ↓ ↓ 该函数指针在上面的●处设置
ra_svn_apply_textdelta libsvn_ra_svn\editorp.c
↓ ↓ ↓ ↓★
svn_stream_set_write(diff_stream, ra_svn_svndiff_handler)
↓ ↓ ↓ ↓ ↓
svn_stream_set_write(..., svn_write_fn_t write_fn)
↓ ↓ ↓ ↓ stream->write_fn = write_fn libsvn_ra_svn\editorp.c
即:stream->write_fn = ra_svn_svndiff_handler
↓ ↓ ↓ ↓
svn_txdelta_to_svndiff2 libsvn_delta\svndiff.c
↓ ↓ ↓ 设置:*handler = window_handler
↓ ↓ ↓
svn_stream_open_readonly libsvn_subr\stream.c
↓ ↓ ↓ 打开要发送的文件
svn_txdelta_send_stream libsvn_delta\text_delta.c
↓ ↓ ↓
svn_txdelta_send_txstream libsvn_delta\text_delta.c
↓ ↓ ↓
(*handler)(window, handler_baton)
↓ ↓ ↓ 该函数指针在上面的★处设置
window_handler libsvn_delta\text_delta.c
↓ ↓ ↓
svn_stream_write libsvn_subr\stream.c
↓ ↓ ↓
stream->write_fn(stream->baton, data, len)
↓ ↓ ↓ 该函数指针在上面的★处设置
ra_svn_svndiff_handler libsvn_ra_svn\editorp.c
↓ ↓ ↓
svn_ra_svn_write_cmd(...,"textdelta-chunk","cs", b->token, &str)
↓ ↓
editor->close_file
↓ ↓
↓ 在(libsvn_ra_svn\editorp.c的)svn_ra_svn_get_editor函数中设置:
ra_svn_editor->close_file = ra_svn_close_file
↓ ↓
ra_svn_close_file libsvn_ra_svn\editorp.c
↓ ↓
svn_ra_svn_write_cmd(b->conn, pool, "close-file", "c(?c)", b->token, text_checksum);
↓ 祥见“命令的发送”
↓
editor->close_edit(edit_baton, pool)
↓
ra_svn_close_edit libsvn_ra_svn\editorp.c
↓
svn_ra_svn_write_cmd libsvn_ra_svn\marshal.c
↓ 见“发送一个命令”
svn_ra_svn_read_cmd_response libsvn_ra_svn\marshal.c
↓ ↓读命令的回应
svn_ra_svn_read_tuple libsvn_ra_svn\marshal.c
↓ ↓
svn_ra_svn_read_item libsvn_ra_svn\marshal.c
↓
eb->callback(eb->callback_baton)
(svn_error_t *ra_svn_end_commit) libsvn_ra_svn\client.c
↓
import函数结束