分析、设计简易云存储系统之间的协议,包括用户客户端和服务器之间的认证协议、数据传输的协议以及新加入线程的认证。
保活连接
服务器和客户端之间应当维护几个固定连接,同时随用户发起的文件传输任务,二者间应建立一些短时间的活动连接。
- 用于命令和文本数据传输的交互连接:此连接用于用户向服务器发送命令和接收服务器返回的文本响应,云存储服务器将会把客户端登录、认证时创建的连接作为该连接,在客户端活动期间,此连接应始终保持活动。
- 客户端用于接收推送消息的连接:单向连接,仅用于客户端接收服务器推送的消息,在客户端活动期间,此连接应始终保持活动。
- 客户端用于上传/下载文件的连接:此连接应在需要时建立,传输任务完成时断开。服务器应当限制每个客户端持有此类连接的数量。
用户登录认证协议
认证流程
- 根据上一篇文章中的需求,要实现的云存储系统应当能保证客户端和服务器之间传输的文件内容不被拦截,或者即时被中间人拦截也无法获取原始信息。要实现这一点,必须对传输的消息进行加密,而通信双方均需具有对加密消息解密的能力,因此双方均需持有密钥。非对称加密方式中,通信双方只有持有私钥的一方能够解密,因此如果采用非对称加密,服务器和客户端均需持有自己的私钥和对方的公钥。一个可行的方案是:当用户注册/登陆时,客户端随机生成一组密钥,并和服务器交换公钥,双方使用对方的公钥加密要传输的信息。
- 在将要实现的云存储系统中,我没有采用上面的方式,而是采用了 AES CFB 对称加密,因为后者相对更容易实现。如果项目完成后还有空余时间,我将更换认证方式。下面要介绍的云存储系统登录认证协议存在漏洞,传输消息可能被拦截、破解。如果中间人拦截到了通信双方使用的随机密钥,则可根据协议构造特定的攻击数据包,获取或破坏用户空间。
- 客户端和服务器建立连接并认证的过程使用协议如下:
- 客户端向服务器发起 TCP 请求,服务器监听到请求并建立 Socket 连接
- 服务器随机生成固定长度的随机密钥 token 并以明文方式发送给客户端
- 客户端接收 token,并使用 token 加密用户名和密码的 MD5 值,将加密后的数据发送给服务器
- 服务器接收客户端发送的认证信息,使用 token 对消息解密,获取用户名和密码的 MD5 值,将密码与数据库中存储的密码 MD5 值比对,验证通过则向客户端发送使用 token 加密的 token,否则主动断开连接
- 客户端等待服务器返回数据或检测到服务器断开连接。如果客户端接收到的数据解密结果不是 token,则主动断开连接并提示认证失败
认证数据协议格式
- 假定现在已有函数
RecvBytes()
,该函数将在后面的《传输协议的实现和封装》中介绍,它将返回一组维持边界的消息,即该函数能够从 Socket 缓冲区中恰好读取出一组符合协议格式的数据。下面定义协议格式。 - 服务器生成固定长度随机密钥后,直接向 socket 缓冲区写入该密钥的明文:
conn.Write([]byte(token))
。因为接下来服务器需要等待客户端响应,因此客户端不需要考虑消息边界,只需要从缓冲区中读取固定长度的字节即可。 - 客户端从 socket 接收固定长度的 token。
- 客户端将用户名和密码的 MD5 值使用 token 加密,发送给服务器的包结构如下。因为 AES CFB 算法解密需要获知明文长度,因此传输的包中应当包含该信息:第一个 8 字节表示用户名明文和密码 MD5 值的总长度,第二个 8 字节表示使用 token 将用户名和密码 MD5 值加密后得到的密文长度,第三个 8 字节表示用户名明文的长度,最后跟着密文。前面的三个 8 字节均为 int64 类型的数据的大端序表示。下面称此格式为 格式0。
1 | --------------------------------------------------------------------- |
- 服务器接收到客户端发送的认证包,按大端序将包的前 24 个字节分别转化为 3 个 int64 类型,并使用 token 解密后面的密文。如果验证成功,则按照下面 “数据传输协议” 中的 “格式1” 将 token 传输给客户端。
数据传输协议
当客户端和服务器建立连接并认证成功后,所有的数据传输操作均应当遵从以下协议。
数据协议格式
- 应用层数据包格式如下,第一个 8 字节表示明文长度,第二个 8 字节表示密文长度 + 16,后跟密文。两个 8 字节均为大端序表示的 int64 类型。下面称此格式为 格式1 。
1 | ---------------------------------------- |
非固定活动连接建立过程
当客户端需要执行传输文件操作时,主动向服务器申请连接。连接过程和上面 “认证数据协议格式” 过程类似:
- 服务器向客户端发送新的随机密钥 token0
- 客户端不再将用户名和密码的 MD5 值发送给服务器,而是将用户名和之前登录认证时获得的 token1 连接在一起,使用 token0 加密,并发送给服务器(发送此段消息时,使用 格式0 )
- 服务器接收到数据后首先解析用户名,发现该用户已登录,则将密码部分字段和该用户登录认证时使用的 token1 比对,符合则返回 token0,否则主动断开连接
- 客户端检查 token0 无误后开始发送相应指令
被动监听推送消息连接过程
此连接和上述 “非固定活动连接建立过程” 类似,区别在于,该连接在整个客户端存活期间均得以保持。为了将此连接和用于传输长数据流的数据区分,服务器将会把用户登录后申请的第一个 “非固定活动连接”(即密码字段填写 token1 的连接)视作客户端用来监听推送消息的连接,而之后的连接均视作用于传输长数据流的连接。
执行指令协议
下面考虑客户端向服务器发送的指令格式,根据指令执行所需要传输的数据流长短,将指令分为端数据流传输指令和长数据流传输指令。其中长数据流传输指令包括文件的上传、下载以及更新。为了简化客户端的实现(正式客户端使用 C++ 编写),指令的传输使用纯文本代替了 JSON 格式。下面用 SEP
表示指令间各个选项的分隔符。SEP
是项目中配置文件可以指定的一个字符串。
短数据流传输指令
- 创建:在云存储空间中创建一个新的文件(夹),初始大小为 0。如果该文件是文本类型,则用户可在线编辑该文件并保存。对于创建操作,需要指定创建的位置、创建的文件(夹)的名称、要创建的是文件还是文件夹。即:
TOUCH <SEP> NAME <SEP> PATH <SEP> ISDIR
,ISDIR
为 0 时创建文件,为 1 时创建文件夹。PATH
为绝对路径,如/home/forec/work/
。 - 复制:将某个文件(夹)拷贝到一个新的位置,即:
CP <SEP> UID <SEP> NEWPATH
,其中UID
为要复制的文件(夹)的 ID,以下UID
均指此意,NEWPATH
为绝对路径。 - 移动:将某个文件(夹)移动到一个新的位置,同时指定新的文件(夹)名,即:
MV <SEP> UID <SEP> NEWNAME <SEP> NEWPATH
,其中NEWPATH
为绝对路径。 - 删除:删除某个文件(夹),若为文件夹,该目录下所有文件(夹)均被删除,即
RM <SEP> UID
。 - 获取文件列表:获取某个目录下的文件(夹)列表,支持按关键词筛选,即:
LS <SEP> RECURSSIVE <SEP> PATH <SEP> ARG0 <SEP> ARG1 <SEP> ...
。其中,RECURSSIVE
为 0 表示仅显示该目录下的文件(夹),为 1 表示递归显示,将整个目录内的所有文件(夹)列表返回给客户端;PATH
为绝对路径,ARGn
为要筛选的关键词,例如在SEP
为+
,筛选条件为路径/home/
下所有文件(夹)名包括ed
和afd
的情况下,可使用LS+1+/home/+ed+afd
。服务器向客户端返回的是一个以\n
划分的纯文本流,每行为一个文件记录,记录各项之间以SEP
划分。 - FORK:Fork其它用户的文件(夹),即:
FORK <SEP> UID <SEP> PASSWORD <SEP> NEWPATH
,其中PASSWORD
是要 Fork 的文件(夹)的提取码,如果不存在提取码则可为任意值,NEWPATH
为要 Fork 到自己存储空间中的路径,UID
为要 Fork 的文件(夹)的唯一标识。 - 改变私有/共享:改变某个文件(夹)的私有或共享性质,即:
CHMOD <SEP> UID <SEP> PRIVATE
,其中PRIVATE
为 0 表示共享,PRIVATE
为 1 表示私有,若PRIVATE
为 1,则系统将随机生成一个 4 位提取码,并将该文件或文件夹下的所有文件提取码设置为该提取码。 - 向其他用户发送消息:即
SEND <SEP> CID <SEP> MESSAGE
,其中CID
为对方的唯一标识符,MESSAGE
为要发送的消息,其中不能包含SEP
。 - 以上所有消息均会返回一个状态码,表示成功或错误信息。状态码将在具体实现时讨论。
长数据流传输指令
下载文件(夹)
- 下载指定
UID
对应的文件(夹),即:GET <SEP> UID <SEP> PASSWORD
。当UID
指向的记录不是用户自己的时,需要使用PASSWORD
作为提取码,如果要下载的UID
是用户自己的文件(夹),则PASSWORD
可以填写任意非空值(这里为了简化服务器对指令的识别,选择了非常幼稚的方法)。每启动一条这样的指令,客户端将主动建立一个新的长数据流传输连接,当客户端创建的长数据流传输连接达到设置的上限时,客户端将阻止创建新的连接,并将新的命令排队,直到此前的长数据流传输连接结束后,才会启动下载线程。 - 服务器与客户端传输线程之间交互的过程:
- 客户端发送 GET 命令
- 服务器接收 GET 命令并检查命令是否合法,包括检查文件是否存在、用户是否具有此文件的读权限等,如果合法返回
"VALID"
,否则返回"NOTPERMITTED"
。 - 服务器发送要下载的文件数目(包括文件夹的数目),使用 8 个字节的
int64
类型表示此数目。 - 对每个文件:
- 服务器发送该文件文件名
- 服务器发送
ISDIR
,即是文件还是文件夹(0或1),若该文件为文件夹(1),则跳过下面的循环,使用 8 个字节的int64
类型表示 1 或 0。 - 服务器开始传输文件
- 文件传输结束,进入下一个循环
上传文件
- 上传一个新的文件,即:
PUT <SEP> UID <SEP> SIZE <SEP> MD5
。其中MD5
为客户端计算的文件的 MD5 值,SIZE
为要上传的文件大小。 - 服务器与客户端传输线程之间交互的过程:
- 客户端发送 TOUCH 指令创建空文件
- 服务器接收客户端发送的 PUT 指令,向创建的空文件写入数据
- 服务器向客户端发送标识码,上传成功(200);开始传输(201);传输出错(203);指令不合法(300);文件尚未创建(301);md5不匹配(403);服务器内部错误(500)
更新文件
- 更新一个已有的文件,即:
UPDATE <SEP> UID <SEP> MD5
。 - 服务器与客户端传输线程之间交互过程:
- 服务器接收客户端发送的 PUT 指令,重新向原文件写入数据
- 若云存储中存在新的 MD5 值,则向客户端发送不需传送(300),客户端停止发送并认为秒传成功
- 若云存储中不存在新的 MD5 值,则向客户端发送开始传送(200),传送结束后,修改原 UID 指向的文件块
- 若发现 UPDATE 指令不合法,则返回错误码
指令的简化
为了简化客户端设计,服务器应当尽量对客户端发送的指令容错。例如:
- 文件下载时,客户端仅需发送 UID,服务器根据 UID 指向的记录,为客户端安排下载策略
- 文件创建/移动/复制时,若目标路径不存在,服务器应向数据库中添加缺失的路径
大文件 MD5 值计算
- 将文件按 4M 分块,最后一块不足 4M 也算一块
- 每块计算 MD5 值,将分别计算出的 MD5 值相连
- 将相连的 MD5 值再计算一次 MD5 值
专栏目录:顶点云(应用)设计与实现
此专栏的上一篇文章:顶点云(应用)项目简介
此专栏的下一篇文章:顶点云(应用)传输协议实现和封装
原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2016/11/13/zenith-cloud-1/) 、作者信息(Forec)和本声明。