16.3 验证请求
在proxy转发请求之前,它必须检查消息的合法性。一个合法的消息必须经过如下的检查:
1、 合法的语法
2、 URI scheme
3、 最大转发次数
4、 (可选)循环检测(loop detection)
5、 proxy-require
6、 proxy-authorization
如果任何一步失败了,proxy都必须作为UAS(8.2)一样,应答一个错误码。
注意proxy并不要求检查合并的请求,并且不能把合并的请求当作一个错误的情况。终端接收到合并的请求会根据8.2.2.2节讲述的内容进行分解。
1、 合法的语法。
请求要能够被服务端事务处理,那么请求就必须是语法无误的。请求中的任何与检查相关的部分或者与请求转发节相关的部分都必须语法严格无误。在检查中,其他部分的严格与否,在检查中都被忽略,并且在转发消息过程中保持不变。例如,proxy不会由于非法的Date头域而拒绝请求。同样的proxy不会在转发请求的时候移去非法的Date头域。这个是为了设计成为可扩展的。以后的扩展可能定义新的方法、新的头域。proxy不能拒绝由于包含了不认识的方法或者不认识的头域而拒绝转发这个请求。
2、 URI scheme检查
如果Request-URI包含一个proxy所不能理解的URI形式,那么proxy应当通过返回一个416来拒绝这个请求(Unsupported URI scheme)。
3、 最大转发次数检查
最大转发次数Max-Forwards头域(20.22)是用来限制转发的次数的。如果请求没有包含Max-Forwards头域,那么这个检查将被忽略。如果请求包含了一个Max-Forwards头域,并且这个头域大于0,那么这个检查就跳过。如果请求包含一个Max-Forwards头域,并且这个头域为0,那么这个proxy不能转发这个请求。如果请求是OPTIONS请求,那么proxy可以作为最终响应者来响应这个请求,并且遵循11节讲述的产生应答。否则proxy应当返回一个483(too many hops)应答。
4、 可选的Loop Detection检查
proxy可以检查看看转发是否可能导致循环。如果请求包含一个Via头域,并且这个头域值,和proxy早先保留的请求的头域值相同,那么这个请求就是以前经过过本proxy。要么这个请求就是循环处理了,要么就是合法的循环处理。要检测请求是否循环处理,proxy可以用branch参数,根据16.6节的第8步来计算,并且和接收到的Via头域做比较。如果参数相等,那么请求就循环了。如果不等,那么请求就是合理的经过,并且继续处理。如果检测到了循环,那么proxy应当返回一个482(LoopDetechted)应答。
5、 Proxy-Require检查
本协议的以后的扩展可能会要求额外的proxy特性。所以终端会在请求中包含一个Proxy-Require头域来表明会使用到那些特性,这样proxy就可以根据Proxy-Require判定自己是否能够支持这些特性。
如果请求包含一个Proxy-Require头域(20.29)并且有一个或者多个本proxy不能理解的option-tags。那么这个proxy必须返回一个420(Bad Extension)错误,并且这个错误应答必须包括一个Unsupported(20.40)头域列明了那些option-tags这个proxy不能支持。
6、 Proxy-Authorization 检查
如果proxy要求在转发请求之前进行身份认证,那么必须根据22.3节中描述的那样进行请求的检查。22.3节也定义了proxy应当怎样处理检查失败的情况。
16.4 路由信息预处理
proxy必须检查请求中的Request-URI部分。如果Request-URI包含了一个本proxy早先放在Record-Route头域中的值(参见16.6,4),proxy必须用Route头域中的最后一个值来替换Request-URI,并且从Route头域中删去这个值。proxy必须接着按照个修改后的请求进行处理。
这个只会在某元素发送请求到proxy(proxy本身可能是一个终端),并且这个请求是基于严格路由的。在接收到请求后重写这个字段是必须的,因为要保持向后兼容性。同时也允许按照本规范实现的元素保护Request-URI通过严格路由的proxy(12.2.1.1节)。
这个要求并没有强制proxy保留状态来检查其早先放在Record-Route头域中的URI。作为替换的方法,proxy只需要保留足够的信息在那些URI里边,这样,在以后出现的时候就能识别了。
如果Request-URI包含了maddr参数,proxy必须检查这个参数来看看是否在proxy配置的可信任的地址列表或者可信任的区域列表中。如果Request-URI包含一个maddr参数,并且这个参数包含了一个proxy可以信任的地址,并且这个请求是通过Request-URI中(指明或者缺省)的端口和协议接收到的,proxy必须抽掉maddr和其他非缺省的端口和通讯参数,并且继续处理。
proxy可能收到一个带有匹配maddr的请求,但是不是在URI中指出的端口和transport接收到的。这个请求需要通过指明的port和transport转发到对应的proxy。
如果Route头域的第一个值就是这个proxy,那么proxy必须从请求中把它移去。
16.5 确定请求的目的
接着,proxy计算请求的目的(或者多个目的地)。目的地集合可以由请求的内容决定或者由绝对位置服务提供。目的地集合中的每一个目的地都由URI来表达。
如果请求中的Request-URI包含了maddr参数,必须把Request-URI放在目标集合中,并且是作为唯一一个目标URI,并且proxy必须按照16.6节中的约定进行处理。
如果Request-URI的区域并非本proxy负责的区域,那么Request-URI必须放在目标集合中,并且作为唯一一个目标URI,并且proxy必须按照16.6节中的约定进行处理。
有很多种情况都会导致proxy收到并非本proxy负责的区域的请求。一个防火墙proxy处理外发的请求(就像HTTPproxy处理外发的请求)就是一个典型的例子。
如果请求的目标集合没有像上边讲述的这样预先设定,那么这就意味着proxy是负责Request-URI所指明的区域的,并且proxy可以用任何机制来决定往哪里发送这个请求。这些机制都可以归结成为访问一个绝对位置服务的形式。这个可能由从SIP注册服务器创建的位置服务器获得信息、读取数据库、查阅服务器、利用其他协议、或者就简单的替代Request-URI来实现。当访问由注册服务器创建的位置服务的时候,在作为索引查询之前,Request-URI必须首先根据10.3节进行规范处理。这些机制的输出结果将作为目的地集合。
如果Request-URI没有提供足够的信息来让proxy能够产生目的地集合,它应当返回一个485(Ambiguous)应答。这个应答应当包含一个Contact头域包含一些应当尝试的新位置。比如,一个到sip:
[email protected]的INVITE可能在某一个proxy是不明确的,因为这个proxy有多个JohnSmith。21.4.23节有细节描述。
任何与这个请求有关的,或者与proxy当前环境有关的信息都可以用来构造目的地集合。例如,由于请求的内容不同或者包头域的不同,可以有不同的目的地集合,又或者不同时间到达的请求也可以有不同的目的地集合,或者不同的时间间隔,上一次失败的请求,甚至是当前proxy的利用率都可以导致目的地集合的不同。
通过这些机制,我们可以有一个可能的目的地列表,他们的URI被增加到目的地集合。每一个目的地只能在目的地列表中出现一次。如果目的URI已经在这个集合中存在了(基于URI类型的相等定义),那么它不能再次增加。
如果原请求的Reuqest-URI指明的区域并非本proxy所负责的区域,那么本proxy不能增加任何额外的目的地到目的地集合。
如果proxy负责Request-URI所指明的区域,那么这个proxy只可以在转发的时候改变请求的Request-URI。如果proxy并非负责这个URI,那么它不会在3xx或者416应答的时候查生递归。
如果原始请求的Request-URI是属于本proxy负责的区域的,那么proxy可以在请求转发的时候增加目的地。他可以在处理过程中,用任何可以获得的信息来决定新的目的地。 例如,proxy可以选择把一个转发应答(3xx)所包含的联系地址合并到目的地集合中。如果proxy使用一个动态的信息源来构造目的地集合(例如访问SIP的注册服务器),它应当在处理请求的过程中监测这个信息源。当有新的目的地出现的时候,就应当加到这个目的地集合里边。就像上边说得,每一个URI只能在集合中出现1次。
只能出现1次的原因是可以降低网络冲突,在合并重定向请求的联系地址的情况下可以防止无限递归的出现。
举例来说,一个简单的位置服务是一个”no-op”(无操作的),返回的目的URI就是输入的请求URI。请求将送到一个特定的下一个节点proxy。在16.6节/6小节定义请求转发中,通过SIP或者SIPS URI表达的,下一个节点的身份, 将在Route的头域最上一层插入。
如果Request-URI是这个proxy所负责的,但是在本proxy中找不到,那么proxy必须返回404(Not Found)应答。
如果目的集合经过上边的处理依旧是空的,那么proxy必须返回一个错误应答,这个错误应答应当是408(暂时不可用)。
16.6 请求转发
当目的地集合不是空的时候,proxy可以开始转发这个请求。有状态的proxy可以按照任意的顺序处理这个目的地集合。它可以顺序处理多个目的地,上一个完成前下一个不能开始。也可以采用并行的处理多个目的地。也可以通过分组的形式,每组之间是串行的,组内是并行的。
通常的处理顺序机制是使用一个Contact头域的qvalue参数来处理(20.10节)。目的地从最高的qvalue开始处理到最低的qvalue。相同qvalue的目的地可以并行处理。
有状态的proxy必须包含针对目的地集合的一个接收到应答和转发出去的原始请求进行匹配的机制。为了完成这样的目的,这个机制是一个由proxy层在转发第一个请求前创建的”response context”(应答上下文)来保障的。
对于每一个目的地,proxy转发请求都遵循下列步骤:
1、 拷贝一个接收到的请求
2、 更新Request-URI
3、 更新Max-Forwards头域
4、 可选增加一个Record-Route头域
5、 可选增加附加的头域
6、 路由信息后处理
7、 决定下一个节点地址、端口、通讯协议。
8、 增加一个Via头域值
9、 如果需要,增加一个Content-Length头域
10、 转发这个新的请求
11、 设置定时器C
下面详细介绍每一步。
1、 拷贝请求
proxy首先把接收到的请求做一个拷贝。拷贝必须包含接收到的请求的全部头域。在接下来的处理步骤中未提及的头域不能删除。拷贝应当保留接收到的请求的头域的顺序。proxy不能用合并的域名来进行域值的重新排序(参见7.3.1)。proxy不能增加、修改、删除消息体。
实际上,在实现中并非只是做一个拷贝;首要的事情是为每一个下一个节点准备一个相同的请求。
2、 Request-URI
在拷贝好的请求中的Request-URI必须用目的地的URI进行替换。如果这个目的URI包含任何在Request-URI中所不能允许的参数,那么这些参数必须被删去。
这个步骤是proxy的本质步骤。proxy通过这个机制来把请求转发到目的地。在某些情况下,接收到的Request-URI会不作更改的添加到目的地集合中。对于这样的目的地来说,上边讲的替换就等于是没有任何操作。
3、 Max-Forwards
如果拷贝的头域包含一个Max-Forwards,proxy必须把这个域值减一。
如果拷贝的头域没有包含一个Max-Forwards头域,proxy必须自己增加一个头域,缺省值是70。现在有一些UA不会在请求中填写Max-Forwards头域。
4、 Record-Route
(假设proxy接收到的这个请求会创建一个对话的情况下),如果希望保留这个请求创建的对话中,后续的请求依旧是要经过本proxy,那么本proxy必须增加一个Record-Route头域值在这个拷贝中,并且增加的这个头域值应当是在其他现存的Record-Route头域之前。通过请求建立的对话可以包含一个预置的Route头域。
如果这个请求已经是一个对话的一部分,proxy如果希望以后这个对话的请求依旧经过本proxy,那么proxy应当增加一个Record-Route头域值。在12节描述的普通的终端操作中,这些Record-Route头域值不会对终端使用的路由集合造成任何影响。
如果请求本身已经在对话中的话,如果proxy不增加一个Record-Route头域在请求的包头,后续的请求也会经过本proxy。但是,如果当终端中断并且重新构造这个对话的时候,本proxy就会从对话所经过的节点中删去。
一个proxy可以在任何请求中增加这个Record-Route头域值。如果请求并没有初始化一个对话,终端将会忽略这个头域值。12节讲述了终端如何使用Record-Route头域来构造Route头域的。
在请求路径上的每一个proxy都是独立的决定是否增加一个Record-Route头域值的-在请求的Record-Route头域上的值并不影响这个proxy决定增加还是不增加Record-Route头域值。
在Record-Route头域中防止的URI必须是SIP或者SIPS URI。这个URI必须包含一个lr参数(参见19.1.1)。这个URI可以和请求将被转发的地方不同。这个URI不应当包含通讯参数,除非该proxy确认在后续请求将会经过的下行节点中,都支持这个通讯参数(比如本地网络等等)。
本proxy提供的这个URI可能会让其他元素(其他proxy)作出路由决定。本proxy,通常,并不知道其他节点的处理能力,所以,它必须严格律己,让自己遵循规范的SIP实现:SIP URI和TCP或者UDP通讯协议。
在Record-Route中的URI必须指向插入它的元素(或者替代元素),这个意思是说通过附件[4]的服务器定位步骤可以顺利找到这个元素,这样后续的请求才能顺利到达同一个SIP元素。如果Request-URI包含一个SIPS URI,或者Route头域的最上的值(经过后续第6步的处理)包含一个SIPS URI,那么插入Record-Route头域的值必须是一个SIPS URI。而且,如果请求不是基于TLS接受的,那么proxy必须增加一个Record-Route头域。在相似的情况下,proxy如果从TLS上接收的请求,但是产生的是一个在Record-Route中或者Route头域最上值中没有SIPS URI的请求(在第6步后处理之后),必须在Record-Route头域中增加一个非SIPS URI。
在安全范畴内的proxy必须在对话中保持这个安全范畴。
当Record-Route头域的URI在应答中又重新到达的时候,如果这个URI值需要重写的时候,这个URI必须是能够唯一确定的URI。(就是说,请求可能会经过这个proxy好几次,造成一个或者多个Record-Route头域值的增加)。16.7节的第8步提供了一个能够让这个URI唯一的一个机制。
这个proxy可以在Record-Route头域中增加一些参数。这些参数在某些请求的应答中会被反射(echo)回来,比如给INVITE请求的200(OK)应答。通过在消息的参数中保持状态比在proxy中保持状态更加有效。
如果proxy想在任何类型的对话中都保持在请求的路径上(比如在跨越防火墙的对话中),它需要给每一个接收到的请求中,都增加Record-Route头域,即使是它所不能理解的方法的请求也要增加,因为这些方法可能是对话相关的,具有对话语义的方法。
在Record-Route头域中增加的URI只是在当这个请求创建对话的时候有效。举一个例子,对于一个对话-有状态的proxy(dialog-stateful proxy),当在对话结束后,如果再收到一个请求,这个请求的Request-URI的值中包含这个URI,那么它就可以选择拒绝这个请求。一个对话状态无关的proxy,当然,没有对话结束的概念,但是他们可以再这个值中填写足够多的信息,这样就可以在以后的请求来到的时候做对话的ID的比较,并且可以选择拒绝不匹配这个信息的请求。终端不能在对话外使用这个对话中的Record-Route头域的URI。参见12节描述的终端使用Record-Route头域的细节。
当proxy需要查看所有对话中的消息的时候,我们就需要Record-routeing。但是,他会降低处理性能和影响扩展性,因此proxy应当只在特定情况下使用record-route。任何初始化一个对话的SIP请求都可以适用Record-Route。在本文档中,只有INVITE请求是可以适用的,以后的扩展文档可能包含其他的方法。
5、 增加附加的头域
在这一步,proxy可能增加其他适当的头域。
6、 处理路由信息
proxy可以有一个本地的策略,这个策略要求请求在传递到目的地之前,必须经历一个proxy集合。这样的proxy必须能够确保所有的类似的proxy都是松路由(loose routers)的。通常,只有当这些proxy都是在相同的区域管理的时候,我们才可能知道这些proxy是否都是松路由的。这个proxy的集合是通过一组URI的集合表示(每一个都包含一个lr参数)。这个集合必须被放置到Route头域中,并且放置在其他头域值之前。如果Route头域不存在,必须增加一个Route头域,包含这组URI的列表。
如果proxy有一个本地策略要求请求经过一个指定的proxy,在压栈Route头域之外的一个方法就是旁路下边的第10步的逻辑转发,而是直接发送这个请求到这个指定的proxy的地址,端口,和协议。如果请求有一个Route头域,这个额外的方法就不能用了,除非它知道下一个节点proxy是一个松路由的节点。否则,使用上边讲的增加Route头域的方法会更有效,更灵活,适应性更好,并且操作更一致。而且,如果Request-URI包含了一个SIPS URI,这个proxy必须用TLS来进行通讯。
如果请求的拷贝中包含了Route头域,这个proxy必须检查这个Route头域的第一个值。如果这个URI并没有包含lr参数,那么proxy必须根据下列步骤修改这个请求:
- proxy必须把Request-URI放在Route头域中的最后一个值。
- proxy必须把第一个Route头域的值放在Request-URI中,并且从Route头域中删去。
把Request-URI添加到Route头域的最后是为了让Request-URI的信息能够通过严格路由的proxy。”Popping”弹掉第一个Route头域值到Request-URI中是为了能够让严格路由元素能够接收到这个请求(并且用它自己的在Request-URI中的URI和在Route顶部下一个节点的URI)。
7、 确定下一个节点的地址,端口和通讯协议。
proxy可以有自己的策略来决定发送请求到特定的IP地址,端口和transport,可以和Route的值或者Request-URI的值无关。当本proxy不能确定对应ip,端口,transport的服务器是一个松路由(loose router)的时候,这样的策略就不能使用了。但是,除了Route头域应当像上边讲的这样使用,我们并不推荐这样的发送请求的机制。
在没有这样一个替代机制的时候,proxy应用附件[4]的步骤来决定应当向哪里发送这个请求。如果proxy重新规格化请求,并且发送到一个像上边6点讲的严格路由的元素,proxy必须应用这些步骤到请求中的Request-URI。否则,如果Route头域存在,proxy必须应用这些步骤到Route头域的第一个值;如果Route不存在,proxy必须应用这些步骤到Request-URI。这些步骤最终得到一个序列集合(地址,port,transport)。与使用那个URI作为附件[4]处理的输入,如果Request-URI指定了一个SIPS URI,那么proxy必须把输入[4]的URI当作是SIPS URI然后遵循[4]的处理步骤进行处理。
就像在附件[4]中讲述的,proxy必须尝试序列集合中的第一组元素,并且依次尝试序列集合中的每一组元素,直到成功为止。
对于每一组的尝试,proxy必须按照这组的通讯要求,对消息进行适当的格式化,并且用一个新的客户端事务(第8到第10点讲述的),进行请求的发送。
由于每一组的发送都是使用心得客户端事务,这就体现了一个新的分支。因而,第8步插入的Via头域中的分支参数必须每组发送的都不一样。
如果客户端事务报告发送请求失败,或者它自身的状态机超时,proxy就应当继续处理序列集合中的下一组元素。当遍历完序列集合之后,请求就不能发送到目的地集合。proxy不需要在应答上下文中放什么应答,然而在别的方面却需要就像从目的地集合收到一个408(Request Timeout)终结响应一样的操作。
8、 增加一个Via头域值
proxy必须在请求的拷贝中增加一个Via头域值,并且在其他Via头域值之前增加。这个值的构造可以参见8.1.1.7。这意味着proxy需要计算自己的分支参数,并且应当是全局唯一的分支,并且包含必要的magic cookie。注意这意味着如果请求循环经过本proxy的时候(也就是数次经过同一个proxy),每次的分支参数都不同。
在proxy构造分支参数的值上,有一个附加的约束,用来进行循环的检测。一个要检测循环的proxy应当创建一个由两部分组成的分支参数。第一部分必须满足8.1.1.7的约束。第二部分是用来做循环检测的,并且是从螺旋中判定是否存在循环(请求数次经过同一个proxy是正常的,这是螺旋,但是如果是循环,那就不正常了。假定proxy是X,CàX,XàY,YàZ,ZàX,XàA,Aà目的地是正常的,但是如果CàX, XàY, YàZ, ZàX, XàY, 这就是循环了)。
循环检测是通过这样的方法检测的:当请求返回给一个proxy,与处理请求相关的字段并未改变,那么这个就是循环了。这个分支参数的后一部分应当反应所有的这些头域(包括所有的Route,Proxy-Require和Proxy-Authorization头域)。这是确保如果请求从别处重新路由回来,而且这些字段改变了,那么这就是一个螺旋而不是循环(参见16.3)。通常建立这个比较值的方法是计算一个hash值,通过基于To tag,From tag,Call-ID头域,收到请求的Request-URI(而不是经过处理过后的Request-URI),Via头域的最上一个,Cseq头域的序列号,任何附加的Proxy-Require或者Proxy-Authorization头域。具体的hash算法是基于实现相关的,但是MD5(RFC1321[35]),用16进制表达,是一个有道理的选择。(基于64位表达的是不太合适的)。
如果proxy希望检测循环,那么”branch”参数必须用包含可能影响处理请求的全部信息构成,包括输入的Request-URI和其他可能会影响proxy处理路由的字段,通过计算得到。这是检测循环所必须的,因为如果请求在路由相关的字段改变以后,重新发回这个服务器,那么新的处理可能会发送到另外的地方,而不是造成一个循环。
在branch参数的计算上,请求的方法不能计算进去。但是作为特例,CANCEL和ACK请求(给非2xx应答的)必须和他们对应的请求有相同的branch值。branch参数用于在服务器处理这些请求的时候体现请求之间的相关性(17.2.3和9.2)
9、 如果需要,增加一个Content-Length头域
如果请求会通过一个基于流的通讯协议发送到下一个节点,并且发送的请求拷贝中没有包含一个Content-Length头域,那么proxy必须增加一个正确的请求包体大小在这个头域(20.14)。
10、 转发请求
一个有状态的proxy必须为这个请求创建一个新的客户端事务(根据17.1节描述的那样),并且指示事务层用第7步指定的地址,端口和协议进行发送。
11、 设定时钟C
为了能够处理INVITE请求没有产生终结应答的情况,TU使用一个定时器(称作定时器C)。当INVITE请求被转发的时候,必须为客户端事务设定一个定时器C。这个定时器C必须大于3分钟。16.7节的2步讲述了这个定时器是如何根据临时应答来更新的,并且16.8节讲述了定时器到时的处理步骤。
16.7 应答的处理
当proxy收到一个应答的时候,它首先尝试定位一个与这个应答匹配的客户端事务(17.1.3)。如果没有匹配,proxy必须作为无状态的proxy来处理这个应答(即使这个应答是信息性质的应答)。如果与应答匹配的客户端事务找到了,那么这个应答将转给这个客户端事务进行处理。
将应答转给对应的客户端事务(或者更通常的说法是发出请求的或者相关的事务),并不是为了更强大的处理能力,它是保证了”晚到”的给INVITE请求的2xx应答能够正确的转发。
当客户端事务把应答交给proxy层,将会执行下列步骤:
1、 寻找适当的应答上下文。
2、 用临时应答来更新定时器C
3、 从最上边移除Via
4、 在应答上下文中增加应答
5、 检查这个应答是否需要立刻发送
6、 如果需要,从应答上下文中选择最好的终结应答。
如果在与这个应答上下文相关的每一个客户端事务都结束的以后,还是没有终结应答转发,那么proxy必须选择从已经收到的应答中,选择转发”best”应答回去。
下列步骤必须在每一个被转发的应答上执行。就像每一个请求有超过一个应答被转发一样:至少有一个终结应答和0个或者多个临时应答。
7、 需要合并认证头域值。
8、 可选的重写Record-Route头域值
9、 转发应答
10、 产生合适的CANCEL请求。
上述每一步在下边有详细的描述:
1、 寻找上下文
proxy通过16.6节定义的方法来在寻找转发原始请求前创建的”应答上下文” 。在这个上下文中进行后续的处理步骤。
2、 为临时应答更新定时器C
对于INVITE事务,如果应答是一个返回码是101到199的临时应答(就是说,除了100的临时应答),proxy必须给这个客户端事务重新设置定时器C。这个定时器可以设置成为其他的值,和原始值不一样,但是这个数字必须大于3分钟。
3、 Via
proxy从应答中移去Via头域中最上的值。
如果在这个应答中没有这个Via头域值,那么应答的含义就是说这个应答不应当被这个proxy转发。本节描述的后续处理步骤也不需要继续处理,而是用8.1.3节定义的UAC处理规则进行处理(传输层处理已经进行)。
这种情况可能会发生,比如,当某一个元素产生一个第10节规定的CANCEL请求。
4、 增加应答到上下文
收到的终结应答都会保存在应答的上下文中,直到收到一个由服务端事务产生的针对这个上下文的终结应答为止。这个应答是从那个服务端事务中收到最佳终结应答的一个候选。即使这个应答不会被选中作为最佳应答,这个应答的信息也需要用来构造成为最佳应答。
如果proxy决定尝试调用3xx应答返回的联系地址,并且把他们添加到目的地集合,它必须在把这个应答添加到应答上下文之前把联系地址从应答中移除。不过,proxy不应当在源请求的Request-URI是一个SIPS URI的情况下,尝试调用非SIPS URI。如果proxy尝试每一个3xx应答给回的联系地址,proxy不应当把这个应答添加到应答上下文中。
从应答中删去联系地址的目的是为了防止下一个节点尝试本proxy已经尝试的地址。
3xx应答可能包含SIP,SIPS和非SIP URI。proxy可以自行决定自己调用那些SIP或者SIPS URI,并且把剩下的放在应答上下文中返回。
如果proxy收到一个对于一个Rquest-URI并非SIP URI的请求的416(不支持的URI scheme)应答,但是原始请求的Request-URI是SIP或者SIPS(就是说,proxy在转发请求的时候自己调换了请求的SIP或者SIPS为一个什么其他的东西),proxy应当增加一个新的URI到目的地集合。这个URI应当是刚才尝试的非SIP URI的SIP URI版本。对于电话URL来说,这个就是把电话URL的电话号码部分放在SIP URI的用户部分,并且设置SIP URI的主机部分成为当前请求发送者的区域。19.1.6节有电话URL到SIP URI的转换细节。
在3XX应答的情况下,如果proxy在416上会产生”递归”(因为尝试SIP或者SIPS URI而导致递归),那么应当在应答上下文中增加这个416应答。
5、 检查转发的应答
当终结应答到达服务端事务的时候,下列应答包必须立刻转发。
- 任何非100(trying)的临时应答
- 任何2xx应答。
如果收到一个6xx应答,那么就不立刻进行转发,如果是有状态的proxy,那么还需要cancel所有的依赖于这个事务的客户端(在10节中描述的那样),并且不能在上下文中创建新的分支。
这个是和RFC 2543的不同之处,2543要求proxy立刻转发6xx应答。对于一个INVITE事务来说,如果立刻转发6xx应答,会使得2xx应答到达别的分支。这个结果就是让UAC在2xx应答之后收到一个6xx应答,这个是不允许发生的。在新的规则下,基于接收到一个6xx应答,proxy应当产生一个CANCEL请求,那么这个会给所有等待的客户端事务一个487应答,这就是6xx应答应当给上行队列的一个结果。
在服务端事务上发送了终结应答之后,下列的应答应当立刻被发送:
- 任何给INVITE请求的2xx应答。
一个有状态的proxy必须不能立刻转发其他的应答。特别是,一个有状态的poxy必须不能转发任何100(Trying)应答。这些应答是作为后续将被转发”最佳”应答的候选,通过上边的”在上下文中增加应答”的步骤增加到应答上下文中。
任何将被立刻发送的应答都必须遵照”7、 需要合并认证头域值。”和”8、可选的重写Record-Route头域值”来处理。
这一步,合并下一步,确保有状态的proxy能够精确转发一个终结应答到一个非-INVITE请求,或者给一个INVITE请求的非2xx应答或者一个或者多个2xx应答。
6、 选择最佳的应答
对于一个有状态的proxy来说,如果根据上边的步骤,没有任何终结应答被立刻发送,并且在客户端事务中的所有的客户端服务都已经终结,那么这个proxy必须发送一个终结应答到一个应答上下文的服务端事务层。
那么这个有状态的proxy就必须从接收到的应答上下文中选择一个”最佳”的终结应答。如果在上下文中没有一个终结应答,那么proxy就必须返回一个408(请求超时)的应答到服务端事务层。
如果应答上下文中有终结应答,那么proxy就必须从这个应答上下文中取得应答来发送。如果应答上下文中有6xx应答,那么就必须选择这个6xx应答。如果没有6xx应答,那么proxy应当选择最小的应答(应答返回代码比较小)。proxy可以选择对应最小应答系列中的任意一个应答(比如2xx系列中的任意一个应答)。proxy应当给那些提供对影响请求的应答更多的机会,比如在4xx系列中,选择401,407,415,420或者484应该稍稍优先一些。
当proxy收到503(Service Unavailable)应答的时候,不应当转发到上行队列中,除非它能够知道这个后续的请求队列都能产生503的应答。换句话说,就是转发503就意味着proxy确实不能处理任何请求,不仅仅是Request-URI里边的这个地址不能处理请求。如果只有某个应答会产生503,proxy应当产生500应当到上行队列中。
被转发的应答都必须遵照”7、 需要合并认证头域值。”和”8、可选的重写Record-Route头域值”来处理。
例如,如果一个proxy转发一个请求到4个地方,并且收到了503,407,501,和404应答,它可能选择407(Proxy Authentication Required)应答。
1xx和2xx应答可能和建立对话有关。当请求没有包含一个To tag,UAC使用在应答中的To tag来区分请求创建的对话的多个应答。如果请求中没有包含To的tag,那么proxy必须不能为1xx或者2xx应答增加这个tag到To头域。一个proxy不能修改1xx或者2xx应答中的To头域的tag字段。
在请求的1xx应答中,如果应答没有To头域的tag字段的时候,由于proxy不能添加tag字段到这个To头域,它就不能产生它自己的非100临时应答。但是它可以把这个请求分支到其他一个UAS上,这个UAS可以和proxy共享同样的元素。这个UAS可以返回它自己的临时应答,进入请求创建早期对话中。这个UAS并没有必要作为一个proxy的严格处理步骤存在。它可以是一个在proxy内部的一个虚拟的UAS实现。
3到6xx的应答是节点到节点传递的。当产生了一个3-6系列的应答的时候,每一个节点都作为UAS一样,产生它自己的应答,通常基于下行队列的应答产生自己的应答。对于每一个节点来说,在转发3到6系列应答回去的时候,如果这个应答没有包含To tag,那么这个节点也应当不改变这个to tag。
当收到的应当包含了一个To tag,那么这个proxy不能够修改这个To tag。
恩,实际上在proxy转发3到6系列应答的时候,如果替换了To tag也不会让上行队列所经过的节点有影响,保留原始的tag值可以有助于调试。
当proxy需要合并多个应答的信息的时候,从这些应答中选取To tag的方式是任意的,并且产生一个新的To tag可能可以使得调试更加容易。举例来说,当合并401(Unauthorized)和407(Proxy Authentication Required)信息的时候,或者合并一个未加密的Contact值和未通过验证的3xx应答的时候,产生一个新的To tag就会让调试比较容易。
7、 合并认证头域值
如果选择的应答是401(Unauthorized)或者407(Proxy Authentication Required),那么proxy就必须从本应答上下文中的所有其他401(Unauthorized)和407应答中搜集WWWAuthenticate和Proxy-Authenticate 头域值。并且把这些信息增加到这个应答中。最后的401或者407应答中可能会包含多个WWWAuthenticate和Proxy-Authenticate头域值。
由于这个请求的一个或者多个目的地可能是需要请求身份验证的,所以这个搜集步骤就是必须的。客户端需要接收到这些所有目的者的应答并且在下一次尝试的时候,为每一个目的地提供相关的身份证明。在26节有相关的说明。
8、 Record-Route
如果最终发送的应答中包含Record-Route头域值,并且是这个proxy所原创提供的值,那么在发送这个应答前,proxy可能需要重写这个值。这提供了一个机制,让proxy能够给下一个上行节点或者下行节点提供非本机的一个URI地址。这种情况是很常见的,比如,在多地址主机系统就非常有用。
如果proxy是通过TLS收到请求的,并且通过非TLS转发出去,proxy必须重写在Record-Route头域中的URI,重写成SIPS URI。如果proxy通过非TLS接收到请求,转发是通过TLS转发的,那么proxy必须重写Record-Route请求头域的URI为一个SIP URI。
proxy提供的新的URI必须满足同样的Record-Route头域的URI约束(16.6节的第四步)。并且遵循下列的修改:
URI不应当包含通讯参数除非proxy知道下一个上行(同下行队列对应的)节点,对于后续的请求都支持相关的通讯参数。
如果proxy打算修改应答中的Record-Route头域,要做的一件事情就是定位插入的Record-Route头域值。如果请求是螺旋经过的,并且proxy在每次螺旋的时候都插入了Record-Route值,在应答中(必须在反向路径中的正确位置)找到正确的需要修改的值就需要一点技巧。上边的规则强调proxy在增加Record-Route头域值的时候是必须增加唯一的URI,这样才能找到正确的一个能够重写。我们推荐proxy为每一个URI的user portion增加一个唯一个proxy实例标志。
当应答到达的时候,proxy修改第一个和proxy实例标志匹配的Record-Route。这个修改导致产生一个在user portion部分去掉proxy实例的URI。到下一个循环回来处理的时候,同样的算法(用参数从上而下的寻找Record-Route头域值)会更改这个proxy插入的下一个Record-Route头域值。
对于proxy增加Record-Route头域值的请求来说,并非每一个应答都包含一个Record-Route头域。如果应答包含一个Record-Route头域,那么就包含这个proxy增加的值。
9、 转发应答
当”合并认证头域”和”Record-Route”步骤完成以后,proxy可以对这个应答做其他的附加处理。但是这个proxy不能增加、修改、删除消息体。并且除非另有指示,除了Via头域值(在16.7节3步)之外,proxy不能删除任何头域值。特别是,proxy不能删除任何可能增加到与处理和这个应答相关的下一个请求的Via头域值的”接收到”的参数。proxy必须把应答传递到跟这个应答上下文相关的服务端事务。这回导致应答发送到最上的Via头域值的地方。如果服务端事务不在处理这个发送,这个节点必须作为无状态proxy转发这个应答到服务端通讯层。服务端事务可能已经标志这个发送应答失败或者内部状态机已经设置成为超时状态。这些错误都应当记录下来用于诊断错误,但是协议并没有要求proxy做补救措施。
proxy必须维持应答上下文直到所有相关事务都已经终结,甚至在发送完成终结应答后还需要维持。
10、 产生CANCEL请求
如果转发的应答是一个终结应答,proxy必须给依赖于这个应答上下文的所有客户端事务,产生CANCEL请求。在收到6xx应答的时候,proxy同样应当为所有等待在这个应答上下文的客户端事务产生CANCEL请求。等待的客户端事务就是收到了临时应答,但是没有收到终结应答(还是出于处理中的状态),并且没有任何CANCEL请求与之相关的请求。产生CANCEL请求请参见9.1节。
对于要求基于转发终结应答而CANCEL的客户端事务并没有保证终端不会收到给一个INVITE的多个200(OK)应答。基于多余一个分支的200(OK)应答可能会在CANCEL请求处理前到达。进一步说,后续的扩展可能会改掉这个产生CANCEL请求的要求。
16.8 处理定时器C
如果定时器C 被出发了,proxy必须要么用另外一个数值重新设定定时器,要么终结客户端事务。如果客户端事务已经收到了临时应答,那么proxy必须产生一个与之匹配的CANCEL请求。如果客户端事务还没有收到临时应答,那么proxy必须就像收到一个408(Request Timeout)一样的处理。
允许proxy重设定定时器就意味着允许proxy基于当前条件(比如服务器利用率等等)动态的扩展事务的生命周期。
16.9 处理通讯层的错误
如果在转发请求(参见18.4)的时候,通讯层报告了一个错误,那么proxy必须就像收到了一个503(Service Unavailable)应答一样的处理。
如果proxy在转发应答的时候接收到错误,那么他就丢弃应答。proxy不能由于通讯的原因而cancel任何和这个应答上下文相关的客户端事务。
如果proxycancel了这些客户端事务,那么一个恶意的或者出错的客户端可以用一个Via头域导致所有的事务都失败。
16.10 CANCEL处理
一个有状态的proxy可以给他自己产生的其他请求在任何时候都产生CANCEL请求(参见9.1遵从接收到对应请求的一个临时应答)。在接收到一个匹配的CANCEL请求的时候,proxy必须取消任何与应答上下文相关的客户端事务。
当INVITE请求有一个Expires头域并且这个头域值已经超时的情况下,一个有状态的proxy可以对这个处于pending的INVITE客户端事务发出CANCEL请求。可是,通常来说,这是不必要的,因为相关的终端会发出结束事务的信号。
当有状态的proxy在它自己的服务端事务上处理CANCEL请求的时候,并没有新的应答上下文会创建。相反,proxy层寻找与这个CANCEL对应请求的现存的应答上下文。如果找到了对应的应答上下文,那么这个节点应当立刻返回一个200(OK)应答给这个CANCEL请求者。在这个情况下,这个节点就像8.2节定义的UAS一样的工作。进一步说,这个节点应当为每一个依赖于这个上下文的客户端事务产生一个CANCEL请求(就像在16.7节第10步描述的那样)。
如果一个应答上下文没有找到,这个节点就无法CANCEL这个请求。它就必须像无状态proxy一样转发这个CANCEL请求(可能这个节点把被CANCEL的请求在先前也当作无状态的proxy转发了)。
16.11 无状态的proxy
当作为无状态的时候,proxy就是一个简单的消息转发者。很多无状态的处理步骤和有状态的时候很类似。不同的地方在下边描述。
一个无状态的proxy并没有事务的概念,或者用于描述有状态proxy行为的应答上下文。相反的是,无状态的proxy处理消息,无论是请求还是应答,都是直接从通讯层处理的(参见18节)。当然,无状态proxy自己也不重发这些消息。他们只是转发他们收到的任何重发的消息(他们本身并没有能力来分辩那些消息是重发的,那些消息是原始消息)。进一步说,当无状态的处理一个请求的时候,这个节点并不产生它自己的100(Trying)或者其他临时应答。
无状态的proxy必须用16.3节描述的那样来验证一个请求。
无状态的proxy必须遵从16.4到16.5节定义的步骤来处理请求,有如下几点例外:
o 无状态的proxy必须从目的地集合中,选择一个并且只能选择一个目的地。这个选择必须是根据消息的头域并且是和服务器时间无关的。特别是,一个重发的请求必须能够每次都转发到相同的目的地。进一步说,CANCEL和非路由的ACK请求必须和他们相关的INVITE请求有相同的转发目的地。
一个无状态的proxy必须遵循16.6节定义的请求处理步骤,并且有下列的不同:
o无状态proxy的branchID来说,必须要求在时间上和空间上都是唯一的。也就是说,无状态的proxy不能简单的使用一个随机数产生器来计算branchID的第一个部分(16.6节8步)。这是由于请求的重发需要相同的值,并且无状态的proxy不能区分重发的请求和原始请求。因此,branch参数的组成部分要求唯一,这样使得重发的时候能够填写相同的值。对于无状态的proxy来说,branch参数必须作为一个重发无关的消息处理参数存在。
我们没有规定无状态proxy采用何种手段保证branchID的唯一性。不过,下列步骤是推荐的方法。proxy检查在接收到请求的最上Via头域值的branchID。如果它是由magic cookie打头的,那么branchID的第一个部分就是当作接收到的branchID的hash值。否则branchID的第一个部分就当作是Via头域的最上值、To头域的tag、From头域的tag,Call-ID头域,Cseq序列号(除了方法部分),接收到的请求的Request-URI的一个hash值。这些头域值在不同事务中总是不一样的。
o所有其他的消息转换(16.6节)必须保证转发重发的请求的时候能够转发到相同的节点。特别是,如果proxy在Record-Route头域中增加了值,或者在Route头域中增加了值,proxy必须在转发重发的请求的时候增加相同的值。至于Via 的branch参数,这就意味着转发必须是基于时间无关的配置或者请求重发无关的属性。
o一个无状态proxy决定转发的地点是像16.6节10步描述的有状态的proxy一样。但是请求是直接交给通讯层发送的,而不是交给客户端事务。
由于一个无状态的proxy必须转发重发的请求到相同的地方,并且增加标志性的branch参数,它只能用消息中本身的信息和时间无关的配置来计算。如果配置状态不是时间无关的(比如,如果路由表更新了),这个改变相关的请求,在这个改动开始以后,到在事务超时的时间范围内,就不能作为无状态的转发了。这个处理这段时间的请求是实现相关的。通常处理的方法,是把这些请求当作事务有状态的进行转发。
无状态的proxy必须不能对CANCEL做特别的处理。CANCEL的处理就像对其他请求的处理一样进行。特别是,一个无状态的proxy使用相同的Route头域来处理CANCEL请求,就像处理其他请求一样。
对于16.7节中定义的应答处理,对于无状态proxy来说,并不适用。当一个应答到达一个无状态proxy,proxy必须检查最上的Via头域值的sent-by参数。如果这个地址和这个proxy一样(就是和proxy插入的先前的请求中的值一样),那么这个proxy必须从应答中移除这个头域值,并且转发这个应答到下一个Via头域值。这个proxy必须不能增加,修改或者删除消息体。除非有特别的说明,proxy必须不能移除其他的头域值。如果地址不匹配本proxy,消息就必须简单的悄悄扔掉。
16.12 Proxy Route处理的总结
在没有本地策略的情况下,proxy对于包含Route头域的请求处理可以归结于如下的步骤:
1、 proxy会检查Request-URI。如果它指向的是本proxy所负责的区域,那么proxy会用位置服务的结果来替换这个URI。否则,proxy不改变这个URI。
2、 proxy会检查Route头域的最上URI。如果这个URI指向这个proxy,这个proxy从Route头域中移除(这个路由节点已经到达)。
3、 proxy会转发请求到最上的Route头域值所标志的URI,或者Request-URI(如果没有Route头域)。proxy通过附件[4]的步骤来产生地址,端口,通讯协议等等用来转发请求所必须的参数。
如果在请求的路径中,没有严格路由节点,Request-URI会始终标志着请求的目的地。
16.12.1例子
16.12.1.1 基本SIP四边形
本例子是一个基本的SIP四边传送,U1->P1->P2->U2,使用proxy来传送。下边是过程。
U1 发送:
INVITE sip:
[email protected] SIP/2.0
Contact: sip:
[email protected]
发给P1,P1是一个外发的proxy。P1并不管辖domain.com,所以它查找DNS并且发送请求到那里。它也增加一个Record-Route头域值:
INVITE sip:
[email protected] SIP/2.0
Contact: sip:
[email protected]
Record-Route: <sip:p1.example.com; lr>
P2收到这个请求。这是domain.com所以它查找位置服务器并且重写Request-URI。它也增加一个Record-Route头域值。请求中没有Route头域,所以它解析一个新的Request-URI来决定把请求发送到哪里。
INVITE sip:
[email protected] SIP/2.0
Contact: sip:
[email protected]
Record-Route: <sip:p2.domain.com; lr>
Record-Route: <sip:p1.example.com; lr>
在u2.domain.com的被叫方接收到这个请求并且返回一个200OK应答:
SIP/2.0 200 OK
Contact: sip:
[email protected]
Record-Route: <sip:p2.domain.com;lr>
Record-Route: <sip:p1.example.com;lr>
u2的被叫方并且设置对话的状态的remote target URI为:
sip:
[email protected]并且它的路由集合是:
(<sip:p2.domain.com;lr>,<sip:p1.example.com;lr>)
这个转发通过P2到P1到U1。现在U1设置它自己的对话状态的remote target URI为:sip:
[email protected]并且它的路由集合是:
(<sip:p1.example.com;lr>,<sip:p2.domain.com;lr>)
由于所有的路由集合元素都包含了lr参数,那么U1构造最后的BYE请求:
BYE sip:
[email protected] SIP/2.0
Route: <sip:p1.example.com;lr>,<sip:p2.domain.com;lr>
就像其他所有的节点(包括proxy)会做的那样,它会使用DNS来解析最上的Route头域的URI值,这样来决定往哪里发送这个请求。这就发到了P1。P1发现Request-URI中标记的URI不是它负责的域,于是它就不改变这个Request-URI。然后看到它是Route头域的第一个值,于是就从Route头域中移去,并且转发这个请求到P2:
BYE sip:
[email protected] SIP/2.0
Route: <sip:p2.domain.com;lr>
P2也发现它自己并非负责这个Request-URI的域(P2负责的是domain.com并非u2.domain.com),于是P2并不改变它。它看到自己在Route的第一个值,于是移去这个,并且向u2.domain.com转发(根据在Request-URI上查找DNS):
BYE sip:
[email protected] SIP/2.0
16.12.1.2 穿越一个严格路由proxy
在这个例子中,对话建立通过4个proxy,每一个增加Record-Route头域值。第三个proxy是由严格路由实现的(RFC 2543)。
U1->P1->P2->P3->P4->U2
INVITE请求到达U2包括了:
INVITE sip:
[email protected] SIP/2.0
Contact: sip:
[email protected]
Record-Route: <sip:p4.domain.com;lr>
Record-Route: <sip:p3.middle.com>
Record-Route: <sip:p2.example.com;lr>
Record-Route: <sip:p1.example.com;lr>
并且U2返回了一个200 OK。接着,U2根据第一个Route头域值发送下边的BYE请求到P4:
BYE sip:
[email protected] SIP/2.0
Route: <sip:p4.domain.com;lr>
Route: <sip:p3.middle.com>
Route: <sip:p2.example.com;lr>
Route: <sip:p1.example.com;lr>
P4并不管辖Request-URI指出的域,于是就不更改这个Reqeust-URI。它发现自己在第一个Route头域中,于是把自己从Route头域移除。然后准备发送请求到现在的第一个Route头域值:sip:p3.middle.com,但是它发现这个URI并没有包含lr参数,于是在发送前,它把这个请求更改成为:
BYE sip:p3.middle.com SIP/2.0
Route: <sip:p2.example.com;lr>
Route: <sip:p1.example.com;lr>
Route: <sip:
[email protected]>
P3是一个严格路由,于是它转发到P2:
BYE sip:p2.example.com;lr SIP/2.0
Route: <sip:p1.example.com;lr>
Route: <sip:
[email protected]>
P2看到Request-URI是它放在Record-Route头域中的值,于是在进一步处理前,它把这个请求改写为:
BYE sip:
[email protected] SIP/2.0
Route: <sip:p1.example.com; lr>
P2自己并不管辖u1.example.com,于是它根据Route头域的值,转发这个请求到P1。
P1发现自己在Route头域的最上,于是把自己移除,得到:
BYE sip:
[email protected] SIP/2.0
由于P1并不管辖u1.example.com并且没有其他的Route头域,P1会基于Request-URI转发这个请求到u1.example.com。
16.12.1.3 重写Record-Route头域值。
在这里例子中,U1和U2是在不同的私有域空间中,并且他们通过proxy P1开始一个对话,这个P1作为不同私有namespace的一个网关存在。
U1->P1->U2
U1发送:
INVITE sip:
[email protected] SIP/2.0
Contact: <sip:
[email protected]>
P1使用自己的定位服务并且发送下边的信息到U2:
INVITE sip:
[email protected] SIP/2.0
Contact: <sip:
[email protected]>
Record-Route: <sip:gateway.rightprivatespace.com;lr>
U2发送200 OK应答回给P1:
SIP/2.0 200 OK
Contact: <sip:
[email protected]>
Record-Route: <sip:gateway.rightprivatespace.com;lr>
P1重写它的Record-Route头域参数,提供成为U1能够使用的参数,并且发送给P1:
SIP/2.0 200 OK
Contact: <sip:
[email protected]>
Record-Route: <sip:gateway.leftprivatespace.com;lr>
稍后,U1发送接下来的BYE到P1:
BYE sip:
[email protected] SIP/2.0
Route: <sip:gateway.leftprivatespace.com;lr>
P1转发到U2:
BYE sip:
[email protected] SIP/2.0
17事务
SIP是一个基于事务处理的协议:部件之间的交互是通过一系列无关的消息交换所完成的。特别是,一个SIP 事务由一个单个请求和这个请求的所有应答组成,这些应答包括了零个或者多个临时应答以及一个或者多个终结应答。在事务中,当请求是一个INVITE(叫做INVITE事务),当终结应答不是一个2xx应答的时候,事务还包括一个ACK。如果应答是一个2xx应答,那么ACK并不认为是事务的一部分。
这个分开的原因是基于传递全部200(OK)应答到UAC的INVITE请求的重要性所决定的。要把所有的200应答全部发给UAC,那么UAS独自负责这些应答的重新传送(参见13.3.1.4),UAC肚子负责挨个ACK确认(参见13.2.2.4)。由于ACK的重传只由UAC发起,所以在自己的事务中进行重传会比较有效。