==== Patch level 3 Source: f7f26e75-0421-0410-8811-d6d8ddf80b0f:/local/twisted:305 [local] Target: bbbe8e31-12d6-0310-92fd-ac37d47ddeeb:/trunk:18594 [mirrored] (svn://svn.twistedmatrix.com/svn/Twisted/trunk) Log: r296@antoine-ubuntu: antoine | 2006-10-30 11:33:09 +0100 creating local branch r297@antoine-ubuntu: antoine | 2006-10-30 12:16:54 +0100 bug fixes + tests r303@antoine-ubuntu: antoine | 2006-11-03 18:46:59 +0100 redo multiline header fix r304@antoine-ubuntu: antoine | 2006-11-03 18:53:30 +0100 Via parsing as class method r305@antoine-ubuntu: antoine | 2006-11-06 10:46:56 +0100 test case for unknown params in Via header === twisted/protocols/sip.py ================================================================== --- twisted/protocols/sip.py (revision 18594) +++ twisted/protocols/sip.py (patch sip-fixes level 3) @@ -94,7 +94,7 @@ 488: "Not Acceptable Here", 491: "Request Pending", 493: "Undecipherable", - + 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", # no donut @@ -102,7 +102,7 @@ 504: "Server Time-out", 505: "SIP Version not supported", 513: "Message Too Large", - + 600: "Busy Everywhere", 603: "Decline", 604: "Does not exist anywhere", @@ -167,7 +167,7 @@ m.update(":") m.update(pszHEntity) HA2 = m.digest().encode('hex') - + m = md5.md5() m.update(HA1) m.update(":") @@ -185,10 +185,10 @@ return hash class Via: - """A SIP Via header.""" + """A SIP Via header. Unknown parameters are ignored.""" def __init__(self, host, port=PORT, transport="UDP", ttl=None, hidden=False, - received=None, rport=None, branch=None, maddr=None): + received=None, rport=None, branch=None, maddr=None, **kargs): self.transport = transport self.host = host self.port = port @@ -205,51 +205,53 @@ s += ";hidden" for n in "ttl", "branch", "maddr", "received", "rport": value = getattr(self, n) - if value == True: + if value is True: s += ";" + n elif value != None: s += ";%s=%s" % (n, value) return s - -def parseViaHeader(value): - """Parse a Via header, returning Via class instance.""" - parts = value.split(";") - sent, params = parts[0], parts[1:] - protocolinfo, by = sent.split(" ", 1) - by = by.strip() - result = {} - pname, pversion, transport = protocolinfo.split("/") - if pname != "SIP" or pversion != "2.0": - raise ValueError, "wrong protocol or version: %r" % value - result["transport"] = transport - if ":" in by: - host, port = by.split(":") - result["port"] = int(port) - result["host"] = host - else: - result["host"] = by - for p in params: - # it's the comment-striping dance! - p = p.strip().split(" ", 1) - if len(p) == 1: - p, comment = p[0], "" + def fromString(cls, value): + """Parse a Via header, returning Via class instance.""" + parts = value.split(";") + sent, params = parts[0], parts[1:] + protocolinfo, by = sent.split(" ", 1) + by = by.strip() + result = {} + pname, pversion, transport = protocolinfo.split("/") + if pname != "SIP" or pversion != "2.0": + raise ValueError, "wrong protocol or version: %r" % value + result["transport"] = transport + if ":" in by: + host, port = by.split(":") + result["port"] = int(port) + result["host"] = host else: - p, comment = p - if p == "hidden": - result["hidden"] = True - continue - parts = p.split("=", 1) - if len(parts) == 1: - name, value = parts[0], True - else: - name, value = parts - if name in ("rport", "ttl"): - value = int(value) - result[name] = value - return Via(**result) + result["host"] = by + for p in params: + # it's the comment-striping dance! + p = p.strip().split(" ", 1) + if len(p) == 1: + p, comment = p[0], "" + else: + p, comment = p + if p == "hidden": + result["hidden"] = True + continue + parts = p.split("=", 1) + if len(parts) == 1: + name, value = parts[0], True + else: + name, value = parts + if name in ("rport", "ttl"): + value = int(value) + result[name] = value + return cls(**result) + fromString = classmethod(fromString) +parseViaHeader = Via.fromString + class URL: """A SIP URL.""" @@ -301,7 +303,7 @@ def __str__(self): return self.toString() - + def __repr__(self): return '' % (self.username, self.password, self.host, self.port, self.transport) @@ -419,12 +421,12 @@ """A SIP message.""" length = None - + def __init__(self): self.headers = util.OrderedDict() # map name to list of values self.body = "" self.finished = 0 - + def addHeader(self, name, value): name = name.lower() name = longHeaders.get(name, name) @@ -434,7 +436,7 @@ def bodyDataReceived(self, data): self.body += data - + def creationFinished(self): if (self.length != None) and (self.length != len(self.body)): raise ValueError, "wrong body length" @@ -465,7 +467,7 @@ else: self.uri = parseURL(uri) cleanRequestURL(self.uri) - + def __repr__(self): return "" % (id(self), self.method, self.uri.toString()) @@ -501,9 +503,9 @@ acceptResponses = 1 acceptRequests = 1 state = "firstline" # or "headers", "body" or "invalid" - + debug = 0 - + def __init__(self, messageReceivedCallback): self.messageReceived = messageReceivedCallback self.reset() @@ -513,12 +515,13 @@ self.length = None # body length self.bodyReceived = 0 # how much of the body we received self.message = None + self.header = None self.setLineMode(remainingData) - + def invalidMessage(self): self.state = "invalid" self.setRawMode() - + def dataDone(self): # clear out any buffered data that may be hanging around self.clearLineBuffer() @@ -530,27 +533,27 @@ if self.length == None: # no content-length header, so end of data signals message done self.messageDone() - elif self.length < self.bodyReceived: + elif self.length > self.bodyReceived: # aborted in the middle self.reset() else: # we have enough data and message wasn't finished? something is wrong raise RuntimeError, "this should never happen" - + def dataReceived(self, data): try: basic.LineReceiver.dataReceived(self, data) except: log.err() self.invalidMessage() - + def handleFirstLine(self, line): """Expected to create self.message.""" raise NotImplementedError def lineLengthExceeded(self, line): self.invalidMessage() - + def lineReceived(self, line): if self.state == "firstline": while line.startswith("\n") or line.startswith("\r"): @@ -580,24 +583,36 @@ else: assert self.state == "headers" if line: - # XXX support multi-line headers - try: - name, value = line.split(":", 1) - except ValueError: - self.invalidMessage() - return - self.message.addHeader(name, value.lstrip()) - if name.lower() == "content-length": + # multi-line header + if line.startswith(" ") or line.startswith("\t"): + name, value = self.header + self.header = name, (value + line.lstrip()) + else: + # new header + if self.header: + self.message.addHeader(*self.header) + self.header = None try: - self.length = int(value.lstrip()) + name, value = line.split(":", 1) except ValueError: self.invalidMessage() return + self.header = name, value.lstrip() + # XXX we assume content-length won't be multiline + if name.lower() == "content-length": + try: + self.length = int(value.lstrip()) + except ValueError: + self.invalidMessage() + return else: # CRLF, we now have message body until self.length bytes, # or if no length was given, until there is no more data # from the connection sending us data. self.state = "body" + if self.header: + self.message.addHeader(*self.header) + self.header = None if self.length == 0: self.messageDone() return @@ -608,7 +623,7 @@ self.message.creationFinished() self.messageReceived(self.message) self.reset(remainingData) - + def rawDataReceived(self, data): assert self.state in ("body", "invalid") if self.state == "invalid": @@ -631,10 +646,10 @@ class Base(protocol.DatagramProtocol): """Base class for SIP clients and servers.""" - + PORT = PORT debug = False - + def __init__(self): self.messages = [] self.parser = MessagesParser(self.addMessage) @@ -658,7 +673,7 @@ def _fixupNAT(self, message, (srcHost, srcPort)): # RFC 2543 6.40.2, senderVia = parseViaHeader(message.headers["via"][0]) - if senderVia.host != srcHost: + if senderVia.host != srcHost: senderVia.received = srcHost if senderVia.port != srcPort: senderVia.rport = srcPort @@ -709,7 +724,7 @@ def handle_response(self, message, addr): """Override to define behavior for responses received. - + @type message: C{Message} @type addr: C{tuple} """ @@ -760,11 +775,11 @@ class Proxy(Base): """SIP proxy.""" - + PORT = PORT locator = None # object implementing ILocator - + def __init__(self, host=None, port=PORT): """Create new instance. @@ -774,7 +789,7 @@ self.host = host or socket.getfqdn() self.port = port Base.__init__(self) - + def getVia(self): """Return value of Via header for this proxy.""" return Via(host=self.host, port=self.port) @@ -797,10 +812,10 @@ d.addErrback(lambda e: self.deliverResponse(self.responseFromRequest(e.code, message)) ) - + def handle_request_default(self, message, (srcHost, srcPort)): """Default request handler. - + Default behaviour for OPTIONS and unknown methods for proxies is to forward message on to the client. @@ -808,9 +823,9 @@ everything. """ def _mungContactHeader(uri, message): - message.headers['contact'][0] = uri.toString() + message.headers['contact'][0] = uri.toString() return self.sendMessage(uri, message) - + viaHeader = self.getVia() if viaHeader.toString() in message.headers["via"]: # must be a loop, so drop message @@ -824,12 +839,12 @@ d = self.locator.getAddress(uri) d.addCallback(self.sendMessage, message) d.addErrback(self._cantForwardRequest, message) - + def _cantForwardRequest(self, error, message): error.trap(LookupError) del message.headers["via"][0] # this'll be us self.deliverResponse(self.responseFromRequest(404, message)) - + def deliverResponse(self, responseMessage): """Deliver response. @@ -838,7 +853,7 @@ # XXX we don't do multicast yet host = destVia.received or destVia.host port = destVia.rport or destVia.port or self.PORT - + destAddr = URL(host=host, port=port) self.sendMessage(destAddr, responseMessage) @@ -848,7 +863,7 @@ for name in ("via", "to", "from", "call-id", "cseq"): response.headers[name] = request.headers.get(name, [])[:] return response - + def handle_response(self, message, addr): """Default response handler.""" v = parseViaHeader(message.headers["via"][0]) @@ -864,7 +879,7 @@ self.gotResponse(message, addr) return self.deliverResponse(message) - + def gotResponse(self, message, addr): """Called with responses that are addressed at this server.""" pass @@ -872,31 +887,31 @@ class IAuthorizer(Interface): def getChallenge(peer): """Generate a challenge the client may respond to. - + @type peer: C{tuple} @param peer: The client's address - + @rtype: C{str} @return: The challenge string """ - + def decode(response): """Create a credentials object from the given response. - + @type response: C{str} """ - + class BasicAuthorizer: """Authorizer for insecure Basic (base64-encoded plaintext) authentication. - + This form of authentication is broken and insecure. Do not use it. """ implements(IAuthorizer) - + def getChallenge(self, peer): return None - + def decode(self, response): # At least one SIP client improperly pads its Base64 encoded messages for i in range(3): @@ -917,12 +932,12 @@ class DigestedCredentials(cred.credentials.UsernameHashedPassword): """Yet Another Simple Digest-MD5 authentication scheme""" - + def __init__(self, username, fields, challenges): self.username = username self.fields = fields self.challenges = challenges - + def checkPassword(self, password): method = 'REGISTER' response = self.fields.get('response') @@ -937,7 +952,7 @@ if opaque not in self.challenges: return False del self.challenges[opaque] - + user, domain = self.username.split('@', 1) if uri is None: uri = 'sip:' + domain @@ -946,17 +961,17 @@ DigestCalcHA1(algo, user, domain, password, nonce, cnonce), nonce, nc, cnonce, qop, method, uri, None, ) - + return expected == response class DigestAuthorizer: CHALLENGE_LIFETIME = 15 - + implements(IAuthorizer) - + def __init__(self): self.outstanding = {} - + def generateNonce(self): c = tuple([random.randrange(sys.maxint) for _ in range(3)]) c = '%d%d%d' % c @@ -975,7 +990,7 @@ 'qop-options="auth"', 'algorithm="MD5"', )) - + def decode(self, response): response = ' '.join(response.splitlines()) parts = response.split(',') @@ -1003,11 +1018,11 @@ authorizers = { 'digest': DigestAuthorizer(), } - + def __init__(self, *args, **kw): Proxy.__init__(self, *args, **kw) self.liveChallenges = {} - + def handle_ACK_request(self, message, (host, port)): # XXX # ACKs are a client's way of indicating they got the last message @@ -1042,7 +1057,7 @@ m.headers.setdefault('www-authenticate', []).append(value) self.deliverResponse(m) - + def login(self, message, host, port): parts = message.headers['authorization'][0].split(None, 1) a = self.authorizers.get(parts[0].lower()) @@ -1067,7 +1082,7 @@ def _cbLogin(self, (i, a, l), message, host, port): # It's stateless, matey. What a joke. self.register(message, host, port) - + def _ebLogin(self, failure, message, host, port): failure.trap(cred.error.UnauthorizedLogin) self.unauthorized(message, host, port) @@ -1137,7 +1152,7 @@ """A simplistic registry for a specific domain.""" implements(IRegistry, ILocator) - + def __init__(self, domain): self.domain = domain # the domain we handle registration for self.users = {} # map username to (IDelayedCall for expiry, address URI) @@ -1150,7 +1165,7 @@ return defer.succeed(url) else: return defer.fail(LookupError("no such user")) - + def getRegistrationInfo(self, userURI): if userURI.host != self.domain: return defer.fail(LookupError("unknown domain")) @@ -1159,7 +1174,7 @@ return defer.succeed(Registration(int(dc.getTime() - time.time()), url)) else: return defer.fail(LookupError("no such user")) - + def _expireRegistration(self, username): try: dc, url = self.users[username] @@ -1169,7 +1184,7 @@ dc.cancel() del self.users[username] return defer.succeed(Registration(0, url)) - + def registerAddress(self, domainURL, logicalURL, physicalURL): if domainURL.host != self.domain: log.msg("Registration for domain we don't handle.") === twisted/test/test_sip.py ================================================================== --- twisted/test/test_sip.py (revision 18594) +++ twisted/test/test_sip.py (patch sip-fixes level 3) @@ -85,6 +85,25 @@ """.replace("\n", "\r\n") +# multiline headers (example from RFC 3261) +response_multiline = """\ +SIP/2.0 200 OK +Via: SIP/2.0/UDP server10.biloxi.com + ;branch=z9hG4bKnashds8;received=192.0.2.3 +Via: SIP/2.0/UDP bigbox3.site3.atlanta.com + ;branch=z9hG4bK77ef4c2312983.1;received=192.0.2.2 +Via: SIP/2.0/UDP pc33.atlanta.com + ;branch=z9hG4bK776asdhds ;received=192.0.2.1 +To: Bob ;tag=a6c85cf +From: Alice ;tag=1928301774 +Call-ID: a84b4c76e66710@pc33.atlanta.com +CSeq: 314159 INVITE +Contact: +Content-Type: application/sdp +Content-Length: 0 +\n""".replace("\n", "\r\n") + + class TestRealm: def requestAvatar(self, avatarId, mind, *interfaces): return sip.IContact, None, lambda: None @@ -155,7 +174,7 @@ self.validateMessage(l[0], "INVITE", "sip:foo", {"from": ["mo"], "to": ["joe"], "content-length": ["4"]}, "abcd") - + def testSimpleResponse(self): l = self.l self.feedMessage(response1) @@ -167,7 +186,23 @@ self.assertEquals(m.body, "") self.assertEquals(m.finished, 1) + def testIncomplete(self): + # test for "aborted" request (body shorter than content-length) + l = self.l + self.feedMessage(request4[:-1]) + self.assertEquals(len(l), 2) + def testMultiLine(self): + l = self.l + self.feedMessage(response_multiline) + self.assertEquals(len(l), 1) + m = l[0] + self.assertEquals(m.headers['via'][0], + "SIP/2.0/UDP server10.biloxi.com;branch=z9hG4bKnashds8;received=192.0.2.3") + self.assertEquals(m.headers['via'][2], + "SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds ;received=192.0.2.1") + + class MessageParsingTestCase2(MessageParsingTestCase): """Same as base class, but feed data char by char.""" @@ -208,7 +243,7 @@ self.assertEquals(v1.transport, v2.transport) self.assertEquals(v1.host, v2.host) self.assertEquals(v1.port, v2.port) - + def testComplex(self): s = "SIP/2.0/UDP first.example.com:4000;ttl=16;maddr=224.2.0.1 ;branch=a7c6a8dlze (Example)" v = sip.parseViaHeader(s) @@ -222,7 +257,7 @@ self.assertEquals(v.toString(), "SIP/2.0/UDP first.example.com:4000;ttl=16;branch=a7c6a8dlze;maddr=224.2.0.1") self.checkRoundtrip(v) - + def testSimple(self): s = "SIP/2.0/UDP example.com;hidden" v = sip.parseViaHeader(s) @@ -236,7 +271,13 @@ self.assertEquals(v.toString(), "SIP/2.0/UDP example.com:5060;hidden") self.checkRoundtrip(v) - + + def testOneIsNotTrue(self): + s = "SIP/2.0/UDP example.com;ttl=1" + v = sip.parseViaHeader(s) + self.assertEquals(v.toString().rsplit(';')[1], "ttl=1") + self.checkRoundtrip(v) + def testSimpler(self): v = sip.Via("example.com") self.checkRoundtrip(v) @@ -253,9 +294,14 @@ self.assertEquals(v.port, 5060) self.assertEquals(v.received, "22.13.1.5") self.assertEquals(v.rport, 12345) - + self.assertNotEquals(v.toString().find("rport=12345"), -1) + def testUnknownParam(self): + s = "SIP/2.0/UDP example.com;alias;eggs;spam=1234" + v = sip.parseViaHeader(s) + self.checkRoundtrip(v) + class URLTestCase(unittest.TestCase): def testRoundtrip(self): @@ -305,8 +351,8 @@ implements(sip.ILocator) def getAddress(self, logicalURL): return defer.fail(LookupError()) - + class ProxyTestCase(unittest.TestCase): def setUp(self): @@ -314,7 +360,7 @@ self.proxy.locator = DummyLocator() self.sent = [] self.proxy.sendMessage = lambda dest, msg: self.sent.append((dest, msg)) - + def testRequestForward(self): r = sip.Request("INVITE", "sip:foo") r.addHeader("via", sip.Via("1.2.3.4").toString()) @@ -334,7 +380,7 @@ "SIP/2.0/UDP 1.2.3.4:5060", "SIP/2.0/UDP 1.2.3.5:5060"]) - + def testReceivedRequestForward(self): r = sip.Request("INVITE", "sip:foo") r.addHeader("via", sip.Via("1.2.3.4").toString()) @@ -346,15 +392,15 @@ self.assertEquals(m.headers["via"], ["SIP/2.0/UDP 127.0.0.1:5060", "SIP/2.0/UDP 1.2.3.4:5060;received=1.1.1.1"]) - + def testResponseWrongVia(self): # first via must match proxy's address r = sip.Response(200) r.addHeader("via", sip.Via("foo.com").toString()) self.proxy.datagramReceived(r.toString(), ("1.1.1.1", 5060)) self.assertEquals(len(self.sent), 0) - + def testResponseForward(self): r = sip.Response(200) r.addHeader("via", sip.Via("127.0.0.1").toString()) @@ -365,7 +411,7 @@ self.assertEquals((dest.host, dest.port), ("client.com", 1234)) self.assertEquals(m.code, 200) self.assertEquals(m.headers["via"], ["SIP/2.0/UDP client.com:1234"]) - + def testReceivedResponseForward(self): r = sip.Response(200) r.addHeader("via", sip.Via("127.0.0.1").toString()) @@ -374,7 +420,7 @@ self.assertEquals(len(self.sent), 1) dest, m = self.sent[0] self.assertEquals((dest.host, dest.port), ("client.com", 5060)) - + def testResponseToUs(self): r = sip.Response(200) r.addHeader("via", sip.Via("127.0.0.1").toString()) @@ -385,10 +431,10 @@ m, addr = l[0] self.assertEquals(len(m.headers.get("via", [])), 0) self.assertEquals(m.code, 200) - + def testLoop(self): r = sip.Request("INVITE", "sip:foo") - r.addHeader("via", sip.Via("1.2.3.4").toString()) + r.addHeader("via", sip.Via("1.2.3.4").toString()) r.addHeader("via", sip.Via("127.0.0.1").toString()) self.proxy.datagramReceived(r.toString(), ("client.com", 5060)) self.assertEquals(self.sent, []) @@ -431,7 +477,7 @@ r.addHeader("contact", "sip:joe@client.com:1234") r.addHeader("via", sip.Via("client.com").toString()) self.proxy.datagramReceived(r.toString(), ("client.com", 5060)) - + def unregister(self): r = sip.Request("REGISTER", "sip:bell.example.com") r.addHeader("to", "sip:joe@bell.example.com") @@ -439,7 +485,7 @@ r.addHeader("via", sip.Via("client.com").toString()) r.addHeader("expires", "0") self.proxy.datagramReceived(r.toString(), ("client.com", 5060)) - + def testRegister(self): self.register() dest, m = self.sent[0] @@ -482,7 +528,7 @@ def testFailedAuthentication(self): self.addPortal() self.register() - + self.assertEquals(len(self.registry.users), 0) self.assertEquals(len(self.sent), 1) dest, m = self.sent[0] @@ -500,13 +546,13 @@ r.addHeader("via", sip.Via("client.com").toString()) r.addHeader("authorization", "Basic " + "userXname:passXword".encode('base64')) self.proxy.datagramReceived(r.toString(), ("client.com", 5060)) - + self.assertEquals(len(self.registry.users), 1) self.assertEquals(len(self.sent), 1) dest, m = self.sent[0] self.assertEquals(m.code, 200) - + def testFailedBasicAuthentication(self): self.addPortal() self.proxy.authorizers = self.proxy.authorizers.copy() @@ -518,7 +564,7 @@ r.addHeader("via", sip.Via("client.com").toString()) r.addHeader("authorization", "Basic " + "userXname:password".encode('base64')) self.proxy.datagramReceived(r.toString(), ("client.com", 5060)) - + self.assertEquals(len(self.registry.users), 0) self.assertEquals(len(self.sent), 1) dest, m = self.sent[0] @@ -546,7 +592,7 @@ d = self.proxy.locator.getAddress(url) self.assertFailure(d, LookupError) return d - + def testNoContactLookup(self): self.register() url = sip.URL(username="jane", host="bell.example.com") @@ -623,8 +669,8 @@ self.assertEquals(r.code, 200) d.addCallback(check) return d - + registerRequest = """ REGISTER sip:intarweb.us SIP/2.0\r Via: SIP/2.0/UDP 192.168.1.100:50609\r @@ -724,7 +770,7 @@ for d, uri in self.registry.users.values(): d.cancel() del self.proxy - + def testChallenge(self): self.proxy.datagramReceived(registerRequest, ("127.0.0.1", 5632)) self.assertEquals(