目录
- 1.什么是XEP-0065?
- 2.为什么会有XEP-0065这个拓展协议?
- 3.XEP-0065协议的内容?
1.什么是XEP-0065
XEP-0065主要是为了解决带外的两个XMPP用户文件传输的一个XMPP的拓展协议。字节流可以是个人对个人的也可以是通过一个特殊目的的代理服务器完成的。主要的的协议是使用TCP协议(UDP也可以作为备选方案提供支持)
2.为什么会有XEP-0065这个拓展协议
- XMPP协议设计是为了在两个网络实体之间进行小块的XML发送,没有设计发送字节流数据。
- 在XMPP网络中一个网络实体需要发送字节流数据到另一个网络实体那(就好比一个即时通讯的软件中,用户之间可能有图片,语音和视频的传输)
- 大多数应用处理字节流的技术是通过SI File Transfer (XEP-0096)和Jingle File Transfer(XEP-0234)的文件传输,为了解决任何两个应用之间有一套通用的协议完成文件传输,因此就有了XEP-0065这个协议
3. XEP-0065的内容
- 确认是否支持该协议
如果网络实体支持该协议,会在请求者发送disco#info的报文之后,响应一个http://jabber.org/protocol/bytestreams的功能标签报文
- 请求者发送询问信息的报文如下:
<iq from='requester@example.com/foo' id='gr91cs53' to='target@example.org/bar' type='get'> <query xmlns='http://jabber.org/protocol/disco#info'/> </iq> ```
- 目标者在收到询问报文之后,如果支持该协议,回应如下报文
<iq from='target@example.org/bar' id='gr91cs53' to='requester@example.com/foo' type='result'> <query xmlns='http://jabber.org/protocol/disco#info'> <identity category='client' type='pc'/> <feature var='http://jabber.org/protocol/bytestreams'/> </query> </iq>
- 请求者发送询问信息的报文如下:
- 发现代理群
在尝试初始化字节流的时候,请求者可能需要找到一个代理。它也能够使用服务器发现和服务器交流
- 请求者发送Service Discovery的请求到服务器
<iq from='requester@example.com/foo' id='pi2b15fv' to='example.com' type='get'> <query xmlns='http://jabber.org/protocol/disco#items'/> </iq>
- 服务器将会返回所有它知道的条目
<iq from='example.com' id='pi2b15fv' to='requester@example.com/foo' type='result'> <query xmlns='http://jabber.org/protocol/disco#items'> <item jid='chatrooms.example.com' name='Chatroom Service'/> <item jid='news.example.com' name='News Feeds'/> <item jid='streamer.example.com' name='File Transfer Relay'/> </query> </iq>
在上述例子中,JID为“stream.example.com"是字节流的代理服务
-
针对服务器返回的结果条目,请求者需要查询决定是否它是一个字节流代理,请求者发送Service Discovery请求给代理
<iq from='requester@example.com/foo' id='yx92b153' to='streamer.example.com' type='get'> <query xmlns='http://jabber.org/protocol/disco#info'/> </iq>
-
代理回应对应的报文,请求检查是否包含的identity标签中的category为proxy并且类型为bytestreams
<iq from='streamer.example.com' id='yx92b153' to='requester@example.com/foo' type='result'> <query xmlns='http://jabber.org/protocol/disco#info'> <identity category='proxy' type='bytestreams' name='File Transfer Relay'/> <feature var='http://jabber.org/protocol/bytestreams'/> </query> </iq>
-
接下来请求者需要请求使用字节流的全地址通过代理,这个通过一个IQ-get到代理服务器包含标签具备字节流的命名空间
<iq from='requester@example.com/foo' id='uj2c15z9' to='streamer.example.com' type='get'> <query xmlns='http://jabber.org/protocol/bytestreams'/> </iq>
-
代理回应一个IQ-result包,包含网络地址,使用作为的子标签,并且标签必须具有以下属性 :
- host : IP地址或是域名(基于TCP的SOCKS5通信,如果是一个IPv6地址,必须遵循RFC 5952)
- jid:JabberID 基于XMPP通信的流主机的JabberID
- port :基于TCP的SOCK5通信的连接端口
代理告知请求者网络地址
<iq from='streamer.example.com' id='uj2c15z9' to='requester@example.com/foo' type='result'> <query xmlns='http://jabber.org/protocol/bytestreams'> <streamhost host='24.24.24.1' jid='streamer.example.com' port='7625'/> </query> </iq>
-
如果请求者因为一些原因没有权限初始化字节流,代理必须返回一个错误给请求者
<iq from='streamer.example.com' id='uj2c15z9' to='requester@example.com/foo' type='error'> <error type='auth'> <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> </error> </iq>
-
如果代理不能够作为一个流主机,代理必须返回一个错误的给请求者,应该是标签
<iq from='requester@example.com/foo' id='uj2c15z9' to='streamer.example.com' type='error'> <error type='cancel'> <not-allowed xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> </error> </iq>
-
- 服务器将会返回所有它知道的条目
- 直接连接
在这种情况下,流主机是一个请求者,也就是说请求者知道流主机的网络地址并且知道什么时候激活字节流
- 直接连接的流程
- 1.请求者和目标者初始化S5B协商通过发送一个IQ-set包,其中包含一全JID<localpart@domain.tld/resource>和请求者/流主机的网络地址不仅仅是打算推送流的StreamID(可选的,计算的DST.ADDR的值)
- 2.目标者打开一个TCP的socket连接给特定网络的流主机/请求者
- 3.目标者请求SOCKS5连接与流主机/请求者
- 4.流主机/请求者通过SOCKS5发送一个成功连接的确认给目标者
- 5.目标者接收S5B的字节流通过回复一个IQ-result的包给请求者,并保留初始的IQ-set包中的id
- 6.请求者和目标者交换字节流数据
- 流程图
- 请求者初始化S5B的协商
- 为了初始化和目标者S5B的协商,请求者发送关于一个或多个流主机的网络地址信息给目标者。在直接连接的情况下,请求者可能包括它自己的信息或者是它自己和代理的信息。在初始化的报文中标签中必须包含一个或多个标签,并且每一个必须有host,jid,port属性。标签必须有sid的属性来指定字节流的Stream ID。可能会有一个mode的属性是tcp(默认是tcp)或udp,标签可能有一个dstaddr的属性(它的值是一个计算的hash值通过SOCKS5 DST.ADDR)
- 报文举例
<iq from='requester@example.com/foo' id='hu3vax16' to='target@example.org/bar' type='set'> <query xmlns='http://jabber.org/protocol/bytestreams' sid='vxf9n471bn46'> <streamhost jid='requester@example.com/foo' host='192.168.4.1' port='5086'/> </query> </iq> ```
- 如果请求是不符合规则的(比如标签不包含sid的属性),目标者将会返回一个,如果目标不想接受字节流,必须返回一个的错误给请求者。
- 拒绝报文如下:
<iq from='target@example.org/bar' id='hu3vax16' to='requester@example.com/foo' type='error'> <error type='modify'> <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> </error> </iq>
- 拒绝报文如下:
- 目标者和流主机/请求者建立SOCKS5连接
- 接着目标者尝试打开一个标准的TCP连接在请求者(流主机)的网络地址(UDP的使用参考其他文件)
如果请求者提供多于一个流主机,目标者应该尝试按顺序连接query标签下的子标签的。Jingle SOCKS5 Bytestreams Transport Method (XEP–0260)修改这个规则通过提供暴露的priority给每一个Streamhost竞选。
- 如果目标者能够打开一个TCP连接和流主机/请求者,必须使用SOCKS5协议去建立SOCKS5连接。遵循RFV 1928,目标可能需要认证为了使用代理。本文不讨论认证过程
- 一旦目标者和流主机/请求者认证成功,将会发送一个连接请求(CMD = X‘01’)为了继续这个协商,必须遵循以下规则:
- 1.主机名必须是SHA1(SID + 请求者的JID + 目标者的JID)被定义为SHA1 哈希算法称为RFC 3174【13】和输出16进制(不是字节码);上提到的和下面的多用户聊天,DST.ADDR值可能直接从请求者到目标者
- 2.端口必须是0
- 3.JID被使用在这哈希函数中必须使用IQ交换中在请求者和目标者中(可能是全JID也可能不是,区别在于有没资源名)
- 4.这个适当的字符串配置文件必须被应用在JID中在使用hash算法之前- 目标建立SOCKS5连接和流主机
CMD = X'01' ATYP = X'03' DST.ADDR = SHA1 Hash of :(SID + 请求者的JID + 目标者的JID) DST.PORT = 0
- 流主机确认连接
STATUS = X'00'
- 当收到目标者回复(遵循RFC 1928第6部分), 流主机必须设置BND.ADDR 和BND.PORT.这个两个值由客户在连接请求中提供。如果目标尝试了但是不能够连接任何流主机并且不希望尝试连接从这边,必须返回错误给请求者,如下:
<iq from='target@example.org/bar' id='hu3vax16' to='requester@example.com/foo' type='error'> <error type='cancel'> <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> </error> </iq>
- 目标建立SOCKS5连接和流主机
- 目标者确认字节流
- 在目标者认证流主机/请求者之后,它能够使用IQ-result包(标签包含一个的子标签指定使用的流主机)应答初始化请求。
- 报文如下:
<iq from='target@example.org/bar' id='hu3vax16' to='requester@example.com/foo' type='result'> <query xmlns='http://jabber.org/protocol/bytestreams' sid='vxf9n471bn46'> <streamhost-used jid='requester@example.com/foo'/> </query> </iq>
至此,请求者知道哪一个流主机被目标者使用和哪一个部分能够使用流主机/请求者来交换数拒通过字节流
- 接着目标者尝试打开一个标准的TCP连接在请求者(流主机)的网络地址(UDP的使用参考其他文件)
- 间接连接
在这种场景下,流主机不是一个请求者而是一个代理,这就意味着请求者需要在发送初始化请求给目标者之前发现流主机的网络地址,需要和流主机协调一个连接同样的方式和目标者一样,需要请求在流主机使用之前激活字节流
- 流程
- 1.作为一个先决条件,请求者选择性的发现流主机的网络地址通过XMPP协议之前讨论的Service Discovery的部分
- 2.请求者初始化S5B协调目标者通过发送IQ-set包(包含JabberID ,网络地址和StreamID,当然计算DST.ADDR 值)
- 3.目标者打开一个TCP连接给选择的流主机
- 4.目标者请求SOCKS5连接在流主机/代理
- 5.流主机发送成功的连接确认给目标者通过SOCKS5
- 6.目标者发送IQ-result给请求者,保存之前初始化的IQ-set中的id
- 7.请求者打开一个TCP socket在流主机
- 8.请求者建立连接通过SOCKS5,使用DST.ADDR和DST.PORT参数按照下面的定义的设置值
- 9.流主机通过SOCKS5发送成功连接的确认给请求者
- 10.请求者发送IQ-set包给流主机请求流主机激活相关StreamID字的节流
- 11.流主机激活字节流(数据是可靠在两个SOCKS5 连接通过代理)
- 12.流主机发送IQ-result给发送者字节流被激活的确认(或是特定的错误)
- 13 请求者和目标者-能够开始使用字节流
- 流程图
- 请求者初始化S5B协调
- 为了和目标者初始化S5B协调,请求者发送一个或多个流主机的网络地址信息,在间接连接中,请求者可能只包括代理的信息或者是代理的和自己的信息
- 标签必须包含一个或是多个标签,每一个必须有host,jid和port属性。query标签必须带有sid的属性指的是特定的字流的ID。query标签可能有mode属性,它的值可能是tcp(默认)和udp。query标签带有dstaddr属性(值是计算的hash值)
- 请求者协调初始化报文:
<iq from='requester@example.com/foo' id='npq71g53' to='target@example.org/bar' type='set'> <query xmlns='http://jabber.org/protocol/bytestreams' sid='vxf9n471bn46'> <streamhost host='24.24.24.1' jid='streamer.example.com' port='7625'/> </query> </iq> ```
- 目标者建立和代理SOCKS5连接
- 接下来目标者尝试打开一个标准的TCP的连接通过代理的网络地址
- 如果目标能够打开TCP连接通过代理,则使用SOCK5协议建立连接。依照1928年的RFC,目标者使用代理的时候需要认证,本文不做讨论。
- 一旦目标者成功被代理认证,将发送一个连接请求(CMD = X’01’)继续这个协调。以下规则将使用:
- 1.主机名必须是SHA1(SID + 请求的JID + 目标者的JID)(SHA1的哈希算法第一在RFC 3174)并且输出16进制加密的
- 2.端口必须为0
- 3.JIDs必须是在IQ交换的JIDs,有可能是全JID和光JID(区别在于有没有资源名)
- 4.适当的文件配置在应用SHA1哈希算法之前使用
- 目标者和流主机建立SOCKS5连接
STATUS = X’00’CMD = X'01' ATYP = X'03' DST.ADDR = SHA1.Hash of (SID + 请求的JID + 目标的JID) DST.PORT = 0 ``` - 流主机确认连接
- 目标确认字节流
- 在目标者与代理之间建立SOCK5连接之后,目标者将回应这个初始化的请求一个IQ-result的请求标签下包含一个子标签表示在使用的流主机
- 目标者告知字节流的请求者
<iq from='target@example.org/bar' id='npq71g53' to='requester@example.com/foo' type='result'> <query xmlns='http://jabber.org/protocol/bytestreams' sid='vxf9n471bn46'> <streamhost-used jid='streamer.example.com'/> </query> </iq>
至此请求者知道哪一个流主机被目标者使用
- 请求者与流主机建立SOCKS5连接
- 不同于直接连接,请求者需要在各个部分使用代理交换数据通过字节流之前和代理建立一个SOCKS5连接。因此,请求者和SOCKS5代理将建立一个连接和目标者相同的方式
- 请求者连接流主机
CMD = X'01' ATYP = X'03' DST.ADDR = SHA1 Hash of : (SID + 请求的JID + 目标的JID) DST.PORT = 0
- 流主机和请求者确认连接
STATUS = X'00'
- 激活字节流
- 接下来请求者需要和代理激活字节流。这个将通过发送一个IQ-set包给代理完成,包括一个activate标签,报文如下
<iq from='requester@example.com/foo' id='oqx6t1c9' to='streamer.example.com' type='set'> <query xmlns='http://jabber.org/protocol/bytestreams' sid='vxf9n471bn46'> <activate>target@example.org/bar</activate> </query> </iq>
- 使用SID和地址在报文中的信息,代理能够激活流通过hash值(SID + 请求的JID + 目标的JID)然后比较结果和DST.ADDR从目标者和接受者中的收到的。尽管这个提供了一个程度的信任,这个从请求者来的激活请求,但是它不能指导激活或是消极的攻击字节流的协调。
- 如果代理能够满足这个需求,必须回应请求者一个IQ-result包,代理告知请求者激活包如下
<iq from='streamer.example.com' id='oqx6t1c9' to='requester@example.com/foo' type='result'/>
至此,各个部分能够开始字节流层面的数据交换了
- 如果代理不能完成这个请求必须回应一个IQ-error给请求者,按照下面条件定义:
- 如果from的地址不满足请求者的全JID
- 要是有一个部分已经连接到代理
- hash值不匹配
- 如果因为一些内部的不规则代理而不能激活字节流
- 接下来请求者需要和代理激活字节流。这个将通过发送一个IQ-set包给代理完成,包括一个activate标签,报文如下
- 流程
- 多人聊天
- 当出现一个多人聊天的会议发送一个S5B邀请给另一个参与者,经常这个多人房间模糊真实的从请求者到目标者的JID和从目标者到请求者的。这就意味着两个部分可能不能够通过同样的信息计算DST.ADDR.为了解决这个问题。请求者应该基于SID,真的JID,和目标者的roomJID计算DST.ADDR.请求者发送一个IQ-set包给目标者房间JID,因为不知道目标者的真实JID
-
请求者初始化协调通过多人房间
<iq from='requester@example.com/foo' id='npq71g53' to='room@conference.example.net/Tget' type='set'> <query xmlns='http://jabber.org/protocol/bytestreams' dstaddr='416781edf1ae50bad01cb8509ba35b43952bc345' sid='yia72g3v49j7'> <streamhost host='24.24.24.1' jid='streamer.example.com' port='7625'/> </query> </iq> ``
-
多人房间将提出IQ-set到目标者真实的JID从请求者的房间JID,多人房间提出初始化请求
<iq from='room@conference.example.net/Rter' id='npq71g53' to='target@example.org/bar' type='set'> <query xmlns='http://jabber.org/protocol/bytestreams' dstaddr='416781edf1ae50bad01cb8509ba35b43952bc345' sid='yia72g3v49j7'> <streamhost host='24.24.24.1' jid='streamer.example.com' port='7625'/> </query> </iq>
-
- 当出现一个多人聊天的会议发送一个S5B邀请给另一个参与者,经常这个多人房间模糊真实的从请求者到目标者的JID和从目标者到请求者的。这就意味着两个部分可能不能够通过同样的信息计算DST.ADDR.为了解决这个问题。请求者应该基于SID,真的JID,和目标者的roomJID计算DST.ADDR.请求者发送一个IQ-set包给目标者房间JID,因为不知道目标者的真实JID
- 直接连接的流程
- 请求者发送Service Discovery的请求到服务器