OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.test.test_sip -*- | |
2 | |
3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 | |
7 """Session Initialization Protocol. | |
8 | |
9 Documented in RFC 2543. | |
10 [Superceded by 3261] | |
11 """ | |
12 | |
13 # system imports | |
14 import socket | |
15 import random | |
16 import time | |
17 import md5 | |
18 import sys | |
19 from zope.interface import implements, Interface | |
20 | |
21 # twisted imports | |
22 from twisted.python import log, util | |
23 from twisted.internet import protocol, defer, reactor | |
24 | |
25 from twisted import cred | |
26 import twisted.cred.credentials | |
27 import twisted.cred.error | |
28 | |
29 # sibling imports | |
30 from twisted.protocols import basic | |
31 | |
32 PORT = 5060 | |
33 | |
34 # SIP headers have short forms | |
35 shortHeaders = {"call-id": "i", | |
36 "contact": "m", | |
37 "content-encoding": "e", | |
38 "content-length": "l", | |
39 "content-type": "c", | |
40 "from": "f", | |
41 "subject": "s", | |
42 "to": "t", | |
43 "via": "v", | |
44 } | |
45 | |
46 longHeaders = {} | |
47 for k, v in shortHeaders.items(): | |
48 longHeaders[v] = k | |
49 del k, v | |
50 | |
51 statusCodes = { | |
52 100: "Trying", | |
53 180: "Ringing", | |
54 181: "Call Is Being Forwarded", | |
55 182: "Queued", | |
56 183: "Session Progress", | |
57 | |
58 200: "OK", | |
59 | |
60 300: "Multiple Choices", | |
61 301: "Moved Permanently", | |
62 302: "Moved Temporarily", | |
63 303: "See Other", | |
64 305: "Use Proxy", | |
65 380: "Alternative Service", | |
66 | |
67 400: "Bad Request", | |
68 401: "Unauthorized", | |
69 402: "Payment Required", | |
70 403: "Forbidden", | |
71 404: "Not Found", | |
72 405: "Method Not Allowed", | |
73 406: "Not Acceptable", | |
74 407: "Proxy Authentication Required", | |
75 408: "Request Timeout", | |
76 409: "Conflict", # Not in RFC3261 | |
77 410: "Gone", | |
78 411: "Length Required", # Not in RFC3261 | |
79 413: "Request Entity Too Large", | |
80 414: "Request-URI Too Large", | |
81 415: "Unsupported Media Type", | |
82 416: "Unsupported URI Scheme", | |
83 420: "Bad Extension", | |
84 421: "Extension Required", | |
85 423: "Interval Too Brief", | |
86 480: "Temporarily Unavailable", | |
87 481: "Call/Transaction Does Not Exist", | |
88 482: "Loop Detected", | |
89 483: "Too Many Hops", | |
90 484: "Address Incomplete", | |
91 485: "Ambiguous", | |
92 486: "Busy Here", | |
93 487: "Request Terminated", | |
94 488: "Not Acceptable Here", | |
95 491: "Request Pending", | |
96 493: "Undecipherable", | |
97 | |
98 500: "Internal Server Error", | |
99 501: "Not Implemented", | |
100 502: "Bad Gateway", # no donut | |
101 503: "Service Unavailable", | |
102 504: "Server Time-out", | |
103 505: "SIP Version not supported", | |
104 513: "Message Too Large", | |
105 | |
106 600: "Busy Everywhere", | |
107 603: "Decline", | |
108 604: "Does not exist anywhere", | |
109 606: "Not Acceptable", | |
110 } | |
111 | |
112 specialCases = { | |
113 'cseq': 'CSeq', | |
114 'call-id': 'Call-ID', | |
115 'www-authenticate': 'WWW-Authenticate', | |
116 } | |
117 | |
118 def dashCapitalize(s): | |
119 ''' Capitalize a string, making sure to treat - as a word seperator ''' | |
120 return '-'.join([ x.capitalize() for x in s.split('-')]) | |
121 | |
122 def unq(s): | |
123 if s[0] == s[-1] == '"': | |
124 return s[1:-1] | |
125 return s | |
126 | |
127 def DigestCalcHA1( | |
128 pszAlg, | |
129 pszUserName, | |
130 pszRealm, | |
131 pszPassword, | |
132 pszNonce, | |
133 pszCNonce, | |
134 ): | |
135 m = md5.md5() | |
136 m.update(pszUserName) | |
137 m.update(":") | |
138 m.update(pszRealm) | |
139 m.update(":") | |
140 m.update(pszPassword) | |
141 HA1 = m.digest() | |
142 if pszAlg == "md5-sess": | |
143 m = md5.md5() | |
144 m.update(HA1) | |
145 m.update(":") | |
146 m.update(pszNonce) | |
147 m.update(":") | |
148 m.update(pszCNonce) | |
149 HA1 = m.digest() | |
150 return HA1.encode('hex') | |
151 | |
152 def DigestCalcResponse( | |
153 HA1, | |
154 pszNonce, | |
155 pszNonceCount, | |
156 pszCNonce, | |
157 pszQop, | |
158 pszMethod, | |
159 pszDigestUri, | |
160 pszHEntity, | |
161 ): | |
162 m = md5.md5() | |
163 m.update(pszMethod) | |
164 m.update(":") | |
165 m.update(pszDigestUri) | |
166 if pszQop == "auth-int": | |
167 m.update(":") | |
168 m.update(pszHEntity) | |
169 HA2 = m.digest().encode('hex') | |
170 | |
171 m = md5.md5() | |
172 m.update(HA1) | |
173 m.update(":") | |
174 m.update(pszNonce) | |
175 m.update(":") | |
176 if pszNonceCount and pszCNonce: # pszQop: | |
177 m.update(pszNonceCount) | |
178 m.update(":") | |
179 m.update(pszCNonce) | |
180 m.update(":") | |
181 m.update(pszQop) | |
182 m.update(":") | |
183 m.update(HA2) | |
184 hash = m.digest().encode('hex') | |
185 return hash | |
186 | |
187 class Via: | |
188 """A SIP Via header.""" | |
189 | |
190 def __init__(self, host, port=PORT, transport="UDP", ttl=None, hidden=False, | |
191 received=None, rport=None, branch=None, maddr=None): | |
192 self.transport = transport | |
193 self.host = host | |
194 self.port = port | |
195 self.ttl = ttl | |
196 self.hidden = hidden | |
197 self.received = received | |
198 self.rport = rport | |
199 self.branch = branch | |
200 self.maddr = maddr | |
201 | |
202 def toString(self): | |
203 s = "SIP/2.0/%s %s:%s" % (self.transport, self.host, self.port) | |
204 if self.hidden: | |
205 s += ";hidden" | |
206 for n in "ttl", "branch", "maddr", "received", "rport": | |
207 value = getattr(self, n) | |
208 if value == True: | |
209 s += ";" + n | |
210 elif value != None: | |
211 s += ";%s=%s" % (n, value) | |
212 return s | |
213 | |
214 | |
215 def parseViaHeader(value): | |
216 """Parse a Via header, returning Via class instance.""" | |
217 parts = value.split(";") | |
218 sent, params = parts[0], parts[1:] | |
219 protocolinfo, by = sent.split(" ", 1) | |
220 by = by.strip() | |
221 result = {} | |
222 pname, pversion, transport = protocolinfo.split("/") | |
223 if pname != "SIP" or pversion != "2.0": | |
224 raise ValueError, "wrong protocol or version: %r" % value | |
225 result["transport"] = transport | |
226 if ":" in by: | |
227 host, port = by.split(":") | |
228 result["port"] = int(port) | |
229 result["host"] = host | |
230 else: | |
231 result["host"] = by | |
232 for p in params: | |
233 # it's the comment-striping dance! | |
234 p = p.strip().split(" ", 1) | |
235 if len(p) == 1: | |
236 p, comment = p[0], "" | |
237 else: | |
238 p, comment = p | |
239 if p == "hidden": | |
240 result["hidden"] = True | |
241 continue | |
242 parts = p.split("=", 1) | |
243 if len(parts) == 1: | |
244 name, value = parts[0], True | |
245 else: | |
246 name, value = parts | |
247 if name in ("rport", "ttl"): | |
248 value = int(value) | |
249 result[name] = value | |
250 return Via(**result) | |
251 | |
252 | |
253 class URL: | |
254 """A SIP URL.""" | |
255 | |
256 def __init__(self, host, username=None, password=None, port=None, | |
257 transport=None, usertype=None, method=None, | |
258 ttl=None, maddr=None, tag=None, other=None, headers=None): | |
259 self.username = username | |
260 self.host = host | |
261 self.password = password | |
262 self.port = port | |
263 self.transport = transport | |
264 self.usertype = usertype | |
265 self.method = method | |
266 self.tag = tag | |
267 self.ttl = ttl | |
268 self.maddr = maddr | |
269 if other == None: | |
270 self.other = [] | |
271 else: | |
272 self.other = other | |
273 if headers == None: | |
274 self.headers = {} | |
275 else: | |
276 self.headers = headers | |
277 | |
278 def toString(self): | |
279 l = []; w = l.append | |
280 w("sip:") | |
281 if self.username != None: | |
282 w(self.username) | |
283 if self.password != None: | |
284 w(":%s" % self.password) | |
285 w("@") | |
286 w(self.host) | |
287 if self.port != None: | |
288 w(":%d" % self.port) | |
289 if self.usertype != None: | |
290 w(";user=%s" % self.usertype) | |
291 for n in ("transport", "ttl", "maddr", "method", "tag"): | |
292 v = getattr(self, n) | |
293 if v != None: | |
294 w(";%s=%s" % (n, v)) | |
295 for v in self.other: | |
296 w(";%s" % v) | |
297 if self.headers: | |
298 w("?") | |
299 w("&".join([("%s=%s" % (specialCases.get(h) or dashCapitalize(h), v)
) for (h, v) in self.headers.items()])) | |
300 return "".join(l) | |
301 | |
302 def __str__(self): | |
303 return self.toString() | |
304 | |
305 def __repr__(self): | |
306 return '<URL %s:%s@%s:%r/%s>' % (self.username, self.password, self.host
, self.port, self.transport) | |
307 | |
308 | |
309 def parseURL(url, host=None, port=None): | |
310 """Return string into URL object. | |
311 | |
312 URIs are of of form 'sip:user@example.com'. | |
313 """ | |
314 d = {} | |
315 if not url.startswith("sip:"): | |
316 raise ValueError("unsupported scheme: " + url[:4]) | |
317 parts = url[4:].split(";") | |
318 userdomain, params = parts[0], parts[1:] | |
319 udparts = userdomain.split("@", 1) | |
320 if len(udparts) == 2: | |
321 userpass, hostport = udparts | |
322 upparts = userpass.split(":", 1) | |
323 if len(upparts) == 1: | |
324 d["username"] = upparts[0] | |
325 else: | |
326 d["username"] = upparts[0] | |
327 d["password"] = upparts[1] | |
328 else: | |
329 hostport = udparts[0] | |
330 hpparts = hostport.split(":", 1) | |
331 if len(hpparts) == 1: | |
332 d["host"] = hpparts[0] | |
333 else: | |
334 d["host"] = hpparts[0] | |
335 d["port"] = int(hpparts[1]) | |
336 if host != None: | |
337 d["host"] = host | |
338 if port != None: | |
339 d["port"] = port | |
340 for p in params: | |
341 if p == params[-1] and "?" in p: | |
342 d["headers"] = h = {} | |
343 p, headers = p.split("?", 1) | |
344 for header in headers.split("&"): | |
345 k, v = header.split("=") | |
346 h[k] = v | |
347 nv = p.split("=", 1) | |
348 if len(nv) == 1: | |
349 d.setdefault("other", []).append(p) | |
350 continue | |
351 name, value = nv | |
352 if name == "user": | |
353 d["usertype"] = value | |
354 elif name in ("transport", "ttl", "maddr", "method", "tag"): | |
355 if name == "ttl": | |
356 value = int(value) | |
357 d[name] = value | |
358 else: | |
359 d.setdefault("other", []).append(p) | |
360 return URL(**d) | |
361 | |
362 | |
363 def cleanRequestURL(url): | |
364 """Clean a URL from a Request line.""" | |
365 url.transport = None | |
366 url.maddr = None | |
367 url.ttl = None | |
368 url.headers = {} | |
369 | |
370 | |
371 def parseAddress(address, host=None, port=None, clean=0): | |
372 """Return (name, uri, params) for From/To/Contact header. | |
373 | |
374 @param clean: remove unnecessary info, usually for From and To headers. | |
375 """ | |
376 address = address.strip() | |
377 # simple 'sip:foo' case | |
378 if address.startswith("sip:"): | |
379 return "", parseURL(address, host=host, port=port), {} | |
380 params = {} | |
381 name, url = address.split("<", 1) | |
382 name = name.strip() | |
383 if name.startswith('"'): | |
384 name = name[1:] | |
385 if name.endswith('"'): | |
386 name = name[:-1] | |
387 url, paramstring = url.split(">", 1) | |
388 url = parseURL(url, host=host, port=port) | |
389 paramstring = paramstring.strip() | |
390 if paramstring: | |
391 for l in paramstring.split(";"): | |
392 if not l: | |
393 continue | |
394 k, v = l.split("=") | |
395 params[k] = v | |
396 if clean: | |
397 # rfc 2543 6.21 | |
398 url.ttl = None | |
399 url.headers = {} | |
400 url.transport = None | |
401 url.maddr = None | |
402 return name, url, params | |
403 | |
404 | |
405 class SIPError(Exception): | |
406 def __init__(self, code, phrase=None): | |
407 if phrase is None: | |
408 phrase = statusCodes[code] | |
409 Exception.__init__(self, "SIP error (%d): %s" % (code, phrase)) | |
410 self.code = code | |
411 self.phrase = phrase | |
412 | |
413 | |
414 class RegistrationError(SIPError): | |
415 """Registration was not possible.""" | |
416 | |
417 | |
418 class Message: | |
419 """A SIP message.""" | |
420 | |
421 length = None | |
422 | |
423 def __init__(self): | |
424 self.headers = util.OrderedDict() # map name to list of values | |
425 self.body = "" | |
426 self.finished = 0 | |
427 | |
428 def addHeader(self, name, value): | |
429 name = name.lower() | |
430 name = longHeaders.get(name, name) | |
431 if name == "content-length": | |
432 self.length = int(value) | |
433 self.headers.setdefault(name,[]).append(value) | |
434 | |
435 def bodyDataReceived(self, data): | |
436 self.body += data | |
437 | |
438 def creationFinished(self): | |
439 if (self.length != None) and (self.length != len(self.body)): | |
440 raise ValueError, "wrong body length" | |
441 self.finished = 1 | |
442 | |
443 def toString(self): | |
444 s = "%s\r\n" % self._getHeaderLine() | |
445 for n, vs in self.headers.items(): | |
446 for v in vs: | |
447 s += "%s: %s\r\n" % (specialCases.get(n) or dashCapitalize(n), v
) | |
448 s += "\r\n" | |
449 s += self.body | |
450 return s | |
451 | |
452 def _getHeaderLine(self): | |
453 raise NotImplementedError | |
454 | |
455 | |
456 class Request(Message): | |
457 """A Request for a URI""" | |
458 | |
459 | |
460 def __init__(self, method, uri, version="SIP/2.0"): | |
461 Message.__init__(self) | |
462 self.method = method | |
463 if isinstance(uri, URL): | |
464 self.uri = uri | |
465 else: | |
466 self.uri = parseURL(uri) | |
467 cleanRequestURL(self.uri) | |
468 | |
469 def __repr__(self): | |
470 return "<SIP Request %d:%s %s>" % (id(self), self.method, self.uri.toStr
ing()) | |
471 | |
472 def _getHeaderLine(self): | |
473 return "%s %s SIP/2.0" % (self.method, self.uri.toString()) | |
474 | |
475 | |
476 class Response(Message): | |
477 """A Response to a URI Request""" | |
478 | |
479 def __init__(self, code, phrase=None, version="SIP/2.0"): | |
480 Message.__init__(self) | |
481 self.code = code | |
482 if phrase == None: | |
483 phrase = statusCodes[code] | |
484 self.phrase = phrase | |
485 | |
486 def __repr__(self): | |
487 return "<SIP Response %d:%s>" % (id(self), self.code) | |
488 | |
489 def _getHeaderLine(self): | |
490 return "SIP/2.0 %s %s" % (self.code, self.phrase) | |
491 | |
492 | |
493 class MessagesParser(basic.LineReceiver): | |
494 """A SIP messages parser. | |
495 | |
496 Expects dataReceived, dataDone repeatedly, | |
497 in that order. Shouldn't be connected to actual transport. | |
498 """ | |
499 | |
500 version = "SIP/2.0" | |
501 acceptResponses = 1 | |
502 acceptRequests = 1 | |
503 state = "firstline" # or "headers", "body" or "invalid" | |
504 | |
505 debug = 0 | |
506 | |
507 def __init__(self, messageReceivedCallback): | |
508 self.messageReceived = messageReceivedCallback | |
509 self.reset() | |
510 | |
511 def reset(self, remainingData=""): | |
512 self.state = "firstline" | |
513 self.length = None # body length | |
514 self.bodyReceived = 0 # how much of the body we received | |
515 self.message = None | |
516 self.setLineMode(remainingData) | |
517 | |
518 def invalidMessage(self): | |
519 self.state = "invalid" | |
520 self.setRawMode() | |
521 | |
522 def dataDone(self): | |
523 # clear out any buffered data that may be hanging around | |
524 self.clearLineBuffer() | |
525 if self.state == "firstline": | |
526 return | |
527 if self.state != "body": | |
528 self.reset() | |
529 return | |
530 if self.length == None: | |
531 # no content-length header, so end of data signals message done | |
532 self.messageDone() | |
533 elif self.length < self.bodyReceived: | |
534 # aborted in the middle | |
535 self.reset() | |
536 else: | |
537 # we have enough data and message wasn't finished? something is wron
g | |
538 raise RuntimeError, "this should never happen" | |
539 | |
540 def dataReceived(self, data): | |
541 try: | |
542 basic.LineReceiver.dataReceived(self, data) | |
543 except: | |
544 log.err() | |
545 self.invalidMessage() | |
546 | |
547 def handleFirstLine(self, line): | |
548 """Expected to create self.message.""" | |
549 raise NotImplementedError | |
550 | |
551 def lineLengthExceeded(self, line): | |
552 self.invalidMessage() | |
553 | |
554 def lineReceived(self, line): | |
555 if self.state == "firstline": | |
556 while line.startswith("\n") or line.startswith("\r"): | |
557 line = line[1:] | |
558 if not line: | |
559 return | |
560 try: | |
561 a, b, c = line.split(" ", 2) | |
562 except ValueError: | |
563 self.invalidMessage() | |
564 return | |
565 if a == "SIP/2.0" and self.acceptResponses: | |
566 # response | |
567 try: | |
568 code = int(b) | |
569 except ValueError: | |
570 self.invalidMessage() | |
571 return | |
572 self.message = Response(code, c) | |
573 elif c == "SIP/2.0" and self.acceptRequests: | |
574 self.message = Request(a, b) | |
575 else: | |
576 self.invalidMessage() | |
577 return | |
578 self.state = "headers" | |
579 return | |
580 else: | |
581 assert self.state == "headers" | |
582 if line: | |
583 # XXX support multi-line headers | |
584 try: | |
585 name, value = line.split(":", 1) | |
586 except ValueError: | |
587 self.invalidMessage() | |
588 return | |
589 self.message.addHeader(name, value.lstrip()) | |
590 if name.lower() == "content-length": | |
591 try: | |
592 self.length = int(value.lstrip()) | |
593 except ValueError: | |
594 self.invalidMessage() | |
595 return | |
596 else: | |
597 # CRLF, we now have message body until self.length bytes, | |
598 # or if no length was given, until there is no more data | |
599 # from the connection sending us data. | |
600 self.state = "body" | |
601 if self.length == 0: | |
602 self.messageDone() | |
603 return | |
604 self.setRawMode() | |
605 | |
606 def messageDone(self, remainingData=""): | |
607 assert self.state == "body" | |
608 self.message.creationFinished() | |
609 self.messageReceived(self.message) | |
610 self.reset(remainingData) | |
611 | |
612 def rawDataReceived(self, data): | |
613 assert self.state in ("body", "invalid") | |
614 if self.state == "invalid": | |
615 return | |
616 if self.length == None: | |
617 self.message.bodyDataReceived(data) | |
618 else: | |
619 dataLen = len(data) | |
620 expectedLen = self.length - self.bodyReceived | |
621 if dataLen > expectedLen: | |
622 self.message.bodyDataReceived(data[:expectedLen]) | |
623 self.messageDone(data[expectedLen:]) | |
624 return | |
625 else: | |
626 self.bodyReceived += dataLen | |
627 self.message.bodyDataReceived(data) | |
628 if self.bodyReceived == self.length: | |
629 self.messageDone() | |
630 | |
631 | |
632 class Base(protocol.DatagramProtocol): | |
633 """Base class for SIP clients and servers.""" | |
634 | |
635 PORT = PORT | |
636 debug = False | |
637 | |
638 def __init__(self): | |
639 self.messages = [] | |
640 self.parser = MessagesParser(self.addMessage) | |
641 | |
642 def addMessage(self, msg): | |
643 self.messages.append(msg) | |
644 | |
645 def datagramReceived(self, data, addr): | |
646 self.parser.dataReceived(data) | |
647 self.parser.dataDone() | |
648 for m in self.messages: | |
649 self._fixupNAT(m, addr) | |
650 if self.debug: | |
651 log.msg("Received %r from %r" % (m.toString(), addr)) | |
652 if isinstance(m, Request): | |
653 self.handle_request(m, addr) | |
654 else: | |
655 self.handle_response(m, addr) | |
656 self.messages[:] = [] | |
657 | |
658 def _fixupNAT(self, message, (srcHost, srcPort)): | |
659 # RFC 2543 6.40.2, | |
660 senderVia = parseViaHeader(message.headers["via"][0]) | |
661 if senderVia.host != srcHost: | |
662 senderVia.received = srcHost | |
663 if senderVia.port != srcPort: | |
664 senderVia.rport = srcPort | |
665 message.headers["via"][0] = senderVia.toString() | |
666 elif senderVia.rport == True: | |
667 senderVia.received = srcHost | |
668 senderVia.rport = srcPort | |
669 message.headers["via"][0] = senderVia.toString() | |
670 | |
671 def deliverResponse(self, responseMessage): | |
672 """Deliver response. | |
673 | |
674 Destination is based on topmost Via header.""" | |
675 destVia = parseViaHeader(responseMessage.headers["via"][0]) | |
676 # XXX we don't do multicast yet | |
677 host = destVia.received or destVia.host | |
678 port = destVia.rport or destVia.port or self.PORT | |
679 destAddr = URL(host=host, port=port) | |
680 self.sendMessage(destAddr, responseMessage) | |
681 | |
682 def responseFromRequest(self, code, request): | |
683 """Create a response to a request message.""" | |
684 response = Response(code) | |
685 for name in ("via", "to", "from", "call-id", "cseq"): | |
686 response.headers[name] = request.headers.get(name, [])[:] | |
687 | |
688 return response | |
689 | |
690 def sendMessage(self, destURL, message): | |
691 """Send a message. | |
692 | |
693 @param destURL: C{URL}. This should be a *physical* URL, not a logical o
ne. | |
694 @param message: The message to send. | |
695 """ | |
696 if destURL.transport not in ("udp", None): | |
697 raise RuntimeError, "only UDP currently supported" | |
698 if self.debug: | |
699 log.msg("Sending %r to %r" % (message.toString(), destURL)) | |
700 self.transport.write(message.toString(), (destURL.host, destURL.port or
self.PORT)) | |
701 | |
702 def handle_request(self, message, addr): | |
703 """Override to define behavior for requests received | |
704 | |
705 @type message: C{Message} | |
706 @type addr: C{tuple} | |
707 """ | |
708 raise NotImplementedError | |
709 | |
710 def handle_response(self, message, addr): | |
711 """Override to define behavior for responses received. | |
712 | |
713 @type message: C{Message} | |
714 @type addr: C{tuple} | |
715 """ | |
716 raise NotImplementedError | |
717 | |
718 | |
719 class IContact(Interface): | |
720 """A user of a registrar or proxy""" | |
721 | |
722 | |
723 class Registration: | |
724 def __init__(self, secondsToExpiry, contactURL): | |
725 self.secondsToExpiry = secondsToExpiry | |
726 self.contactURL = contactURL | |
727 | |
728 class IRegistry(Interface): | |
729 """Allows registration of logical->physical URL mapping.""" | |
730 | |
731 def registerAddress(domainURL, logicalURL, physicalURL): | |
732 """Register the physical address of a logical URL. | |
733 | |
734 @return: Deferred of C{Registration} or failure with RegistrationError. | |
735 """ | |
736 | |
737 def unregisterAddress(domainURL, logicalURL, physicalURL): | |
738 """Unregister the physical address of a logical URL. | |
739 | |
740 @return: Deferred of C{Registration} or failure with RegistrationError. | |
741 """ | |
742 | |
743 def getRegistrationInfo(logicalURL): | |
744 """Get registration info for logical URL. | |
745 | |
746 @return: Deferred of C{Registration} object or failure of LookupError. | |
747 """ | |
748 | |
749 | |
750 class ILocator(Interface): | |
751 """Allow looking up physical address for logical URL.""" | |
752 | |
753 def getAddress(logicalURL): | |
754 """Return physical URL of server for logical URL of user. | |
755 | |
756 @param logicalURL: a logical C{URL}. | |
757 @return: Deferred which becomes URL or fails with LookupError. | |
758 """ | |
759 | |
760 | |
761 class Proxy(Base): | |
762 """SIP proxy.""" | |
763 | |
764 PORT = PORT | |
765 | |
766 locator = None # object implementing ILocator | |
767 | |
768 def __init__(self, host=None, port=PORT): | |
769 """Create new instance. | |
770 | |
771 @param host: our hostname/IP as set in Via headers. | |
772 @param port: our port as set in Via headers. | |
773 """ | |
774 self.host = host or socket.getfqdn() | |
775 self.port = port | |
776 Base.__init__(self) | |
777 | |
778 def getVia(self): | |
779 """Return value of Via header for this proxy.""" | |
780 return Via(host=self.host, port=self.port) | |
781 | |
782 def handle_request(self, message, addr): | |
783 # send immediate 100/trying message before processing | |
784 #self.deliverResponse(self.responseFromRequest(100, message)) | |
785 f = getattr(self, "handle_%s_request" % message.method, None) | |
786 if f is None: | |
787 f = self.handle_request_default | |
788 try: | |
789 d = f(message, addr) | |
790 except SIPError, e: | |
791 self.deliverResponse(self.responseFromRequest(e.code, message)) | |
792 except: | |
793 log.err() | |
794 self.deliverResponse(self.responseFromRequest(500, message)) | |
795 else: | |
796 if d is not None: | |
797 d.addErrback(lambda e: | |
798 self.deliverResponse(self.responseFromRequest(e.code, messag
e)) | |
799 ) | |
800 | |
801 def handle_request_default(self, message, (srcHost, srcPort)): | |
802 """Default request handler. | |
803 | |
804 Default behaviour for OPTIONS and unknown methods for proxies | |
805 is to forward message on to the client. | |
806 | |
807 Since at the moment we are stateless proxy, thats basically | |
808 everything. | |
809 """ | |
810 def _mungContactHeader(uri, message): | |
811 message.headers['contact'][0] = uri.toString() | |
812 return self.sendMessage(uri, message) | |
813 | |
814 viaHeader = self.getVia() | |
815 if viaHeader.toString() in message.headers["via"]: | |
816 # must be a loop, so drop message | |
817 log.msg("Dropping looped message.") | |
818 return | |
819 | |
820 message.headers["via"].insert(0, viaHeader.toString()) | |
821 name, uri, tags = parseAddress(message.headers["to"][0], clean=1) | |
822 | |
823 # this is broken and needs refactoring to use cred | |
824 d = self.locator.getAddress(uri) | |
825 d.addCallback(self.sendMessage, message) | |
826 d.addErrback(self._cantForwardRequest, message) | |
827 | |
828 def _cantForwardRequest(self, error, message): | |
829 error.trap(LookupError) | |
830 del message.headers["via"][0] # this'll be us | |
831 self.deliverResponse(self.responseFromRequest(404, message)) | |
832 | |
833 def deliverResponse(self, responseMessage): | |
834 """Deliver response. | |
835 | |
836 Destination is based on topmost Via header.""" | |
837 destVia = parseViaHeader(responseMessage.headers["via"][0]) | |
838 # XXX we don't do multicast yet | |
839 host = destVia.received or destVia.host | |
840 port = destVia.rport or destVia.port or self.PORT | |
841 | |
842 destAddr = URL(host=host, port=port) | |
843 self.sendMessage(destAddr, responseMessage) | |
844 | |
845 def responseFromRequest(self, code, request): | |
846 """Create a response to a request message.""" | |
847 response = Response(code) | |
848 for name in ("via", "to", "from", "call-id", "cseq"): | |
849 response.headers[name] = request.headers.get(name, [])[:] | |
850 return response | |
851 | |
852 def handle_response(self, message, addr): | |
853 """Default response handler.""" | |
854 v = parseViaHeader(message.headers["via"][0]) | |
855 if (v.host, v.port) != (self.host, self.port): | |
856 # we got a message not intended for us? | |
857 # XXX note this check breaks if we have multiple external IPs | |
858 # yay for suck protocols | |
859 log.msg("Dropping incorrectly addressed message") | |
860 return | |
861 del message.headers["via"][0] | |
862 if not message.headers["via"]: | |
863 # this message is addressed to us | |
864 self.gotResponse(message, addr) | |
865 return | |
866 self.deliverResponse(message) | |
867 | |
868 def gotResponse(self, message, addr): | |
869 """Called with responses that are addressed at this server.""" | |
870 pass | |
871 | |
872 class IAuthorizer(Interface): | |
873 def getChallenge(peer): | |
874 """Generate a challenge the client may respond to. | |
875 | |
876 @type peer: C{tuple} | |
877 @param peer: The client's address | |
878 | |
879 @rtype: C{str} | |
880 @return: The challenge string | |
881 """ | |
882 | |
883 def decode(response): | |
884 """Create a credentials object from the given response. | |
885 | |
886 @type response: C{str} | |
887 """ | |
888 | |
889 class BasicAuthorizer: | |
890 """Authorizer for insecure Basic (base64-encoded plaintext) authentication. | |
891 | |
892 This form of authentication is broken and insecure. Do not use it. | |
893 """ | |
894 | |
895 implements(IAuthorizer) | |
896 | |
897 def getChallenge(self, peer): | |
898 return None | |
899 | |
900 def decode(self, response): | |
901 # At least one SIP client improperly pads its Base64 encoded messages | |
902 for i in range(3): | |
903 try: | |
904 creds = (response + ('=' * i)).decode('base64') | |
905 except: | |
906 pass | |
907 else: | |
908 break | |
909 else: | |
910 # Totally bogus | |
911 raise SIPError(400) | |
912 p = creds.split(':', 1) | |
913 if len(p) == 2: | |
914 return cred.credentials.UsernamePassword(*p) | |
915 raise SIPError(400) | |
916 | |
917 | |
918 class DigestedCredentials(cred.credentials.UsernameHashedPassword): | |
919 """Yet Another Simple Digest-MD5 authentication scheme""" | |
920 | |
921 def __init__(self, username, fields, challenges): | |
922 self.username = username | |
923 self.fields = fields | |
924 self.challenges = challenges | |
925 | |
926 def checkPassword(self, password): | |
927 method = 'REGISTER' | |
928 response = self.fields.get('response') | |
929 uri = self.fields.get('uri') | |
930 nonce = self.fields.get('nonce') | |
931 cnonce = self.fields.get('cnonce') | |
932 nc = self.fields.get('nc') | |
933 algo = self.fields.get('algorithm', 'MD5') | |
934 qop = self.fields.get('qop-options', 'auth') | |
935 opaque = self.fields.get('opaque') | |
936 | |
937 if opaque not in self.challenges: | |
938 return False | |
939 del self.challenges[opaque] | |
940 | |
941 user, domain = self.username.split('@', 1) | |
942 if uri is None: | |
943 uri = 'sip:' + domain | |
944 | |
945 expected = DigestCalcResponse( | |
946 DigestCalcHA1(algo, user, domain, password, nonce, cnonce), | |
947 nonce, nc, cnonce, qop, method, uri, None, | |
948 ) | |
949 | |
950 return expected == response | |
951 | |
952 class DigestAuthorizer: | |
953 CHALLENGE_LIFETIME = 15 | |
954 | |
955 implements(IAuthorizer) | |
956 | |
957 def __init__(self): | |
958 self.outstanding = {} | |
959 | |
960 def generateNonce(self): | |
961 c = tuple([random.randrange(sys.maxint) for _ in range(3)]) | |
962 c = '%d%d%d' % c | |
963 return c | |
964 | |
965 def generateOpaque(self): | |
966 return str(random.randrange(sys.maxint)) | |
967 | |
968 def getChallenge(self, peer): | |
969 c = self.generateNonce() | |
970 o = self.generateOpaque() | |
971 self.outstanding[o] = c | |
972 return ','.join(( | |
973 'nonce="%s"' % c, | |
974 'opaque="%s"' % o, | |
975 'qop-options="auth"', | |
976 'algorithm="MD5"', | |
977 )) | |
978 | |
979 def decode(self, response): | |
980 response = ' '.join(response.splitlines()) | |
981 parts = response.split(',') | |
982 auth = dict([(k.strip(), unq(v.strip())) for (k, v) in [p.split('=', 1)
for p in parts]]) | |
983 try: | |
984 username = auth['username'] | |
985 except KeyError: | |
986 raise SIPError(401) | |
987 try: | |
988 return DigestedCredentials(username, auth, self.outstanding) | |
989 except: | |
990 raise SIPError(400) | |
991 | |
992 | |
993 class RegisterProxy(Proxy): | |
994 """A proxy that allows registration for a specific domain. | |
995 | |
996 Unregistered users won't be handled. | |
997 """ | |
998 | |
999 portal = None | |
1000 | |
1001 registry = None # should implement IRegistry | |
1002 | |
1003 authorizers = { | |
1004 'digest': DigestAuthorizer(), | |
1005 } | |
1006 | |
1007 def __init__(self, *args, **kw): | |
1008 Proxy.__init__(self, *args, **kw) | |
1009 self.liveChallenges = {} | |
1010 | |
1011 def handle_ACK_request(self, message, (host, port)): | |
1012 # XXX | |
1013 # ACKs are a client's way of indicating they got the last message | |
1014 # Responding to them is not a good idea. | |
1015 # However, we should keep track of terminal messages and re-transmit | |
1016 # if no ACK is received. | |
1017 pass | |
1018 | |
1019 def handle_REGISTER_request(self, message, (host, port)): | |
1020 """Handle a registration request. | |
1021 | |
1022 Currently registration is not proxied. | |
1023 """ | |
1024 if self.portal is None: | |
1025 # There is no portal. Let anyone in. | |
1026 self.register(message, host, port) | |
1027 else: | |
1028 # There is a portal. Check for credentials. | |
1029 if not message.headers.has_key("authorization"): | |
1030 return self.unauthorized(message, host, port) | |
1031 else: | |
1032 return self.login(message, host, port) | |
1033 | |
1034 def unauthorized(self, message, host, port): | |
1035 m = self.responseFromRequest(401, message) | |
1036 for (scheme, auth) in self.authorizers.iteritems(): | |
1037 chal = auth.getChallenge((host, port)) | |
1038 if chal is None: | |
1039 value = '%s realm="%s"' % (scheme.title(), self.host) | |
1040 else: | |
1041 value = '%s %s,realm="%s"' % (scheme.title(), chal, self.host) | |
1042 m.headers.setdefault('www-authenticate', []).append(value) | |
1043 self.deliverResponse(m) | |
1044 | |
1045 | |
1046 def login(self, message, host, port): | |
1047 parts = message.headers['authorization'][0].split(None, 1) | |
1048 a = self.authorizers.get(parts[0].lower()) | |
1049 if a: | |
1050 try: | |
1051 c = a.decode(parts[1]) | |
1052 except SIPError: | |
1053 raise | |
1054 except: | |
1055 log.err() | |
1056 self.deliverResponse(self.responseFromRequest(500, message)) | |
1057 else: | |
1058 c.username += '@' + self.host | |
1059 self.portal.login(c, None, IContact | |
1060 ).addCallback(self._cbLogin, message, host, port | |
1061 ).addErrback(self._ebLogin, message, host, port | |
1062 ).addErrback(log.err | |
1063 ) | |
1064 else: | |
1065 self.deliverResponse(self.responseFromRequest(501, message)) | |
1066 | |
1067 def _cbLogin(self, (i, a, l), message, host, port): | |
1068 # It's stateless, matey. What a joke. | |
1069 self.register(message, host, port) | |
1070 | |
1071 def _ebLogin(self, failure, message, host, port): | |
1072 failure.trap(cred.error.UnauthorizedLogin) | |
1073 self.unauthorized(message, host, port) | |
1074 | |
1075 def register(self, message, host, port): | |
1076 """Allow all users to register""" | |
1077 name, toURL, params = parseAddress(message.headers["to"][0], clean=1) | |
1078 contact = None | |
1079 if message.headers.has_key("contact"): | |
1080 contact = message.headers["contact"][0] | |
1081 | |
1082 if message.headers.get("expires", [None])[0] == "0": | |
1083 self.unregister(message, toURL, contact) | |
1084 else: | |
1085 # XXX Check expires on appropriate URL, and pass it to registry | |
1086 # instead of having registry hardcode it. | |
1087 if contact is not None: | |
1088 name, contactURL, params = parseAddress(contact, host=host, port
=port) | |
1089 d = self.registry.registerAddress(message.uri, toURL, contactURL
) | |
1090 else: | |
1091 d = self.registry.getRegistrationInfo(toURL) | |
1092 d.addCallbacks(self._cbRegister, self._ebRegister, | |
1093 callbackArgs=(message,), | |
1094 errbackArgs=(message,) | |
1095 ) | |
1096 | |
1097 def _cbRegister(self, registration, message): | |
1098 response = self.responseFromRequest(200, message) | |
1099 if registration.contactURL != None: | |
1100 response.addHeader("contact", registration.contactURL.toString()) | |
1101 response.addHeader("expires", "%d" % registration.secondsToExpiry) | |
1102 response.addHeader("content-length", "0") | |
1103 self.deliverResponse(response) | |
1104 | |
1105 def _ebRegister(self, error, message): | |
1106 error.trap(RegistrationError, LookupError) | |
1107 # XXX return error message, and alter tests to deal with | |
1108 # this, currently tests assume no message sent on failure | |
1109 | |
1110 def unregister(self, message, toURL, contact): | |
1111 try: | |
1112 expires = int(message.headers["expires"][0]) | |
1113 except ValueError: | |
1114 self.deliverResponse(self.responseFromRequest(400, message)) | |
1115 else: | |
1116 if expires == 0: | |
1117 if contact == "*": | |
1118 contactURL = "*" | |
1119 else: | |
1120 name, contactURL, params = parseAddress(contact) | |
1121 d = self.registry.unregisterAddress(message.uri, toURL, contactU
RL) | |
1122 d.addCallback(self._cbUnregister, message | |
1123 ).addErrback(self._ebUnregister, message | |
1124 ) | |
1125 | |
1126 def _cbUnregister(self, registration, message): | |
1127 msg = self.responseFromRequest(200, message) | |
1128 msg.headers.setdefault('contact', []).append(registration.contactURL.toS
tring()) | |
1129 msg.addHeader("expires", "0") | |
1130 self.deliverResponse(msg) | |
1131 | |
1132 def _ebUnregister(self, registration, message): | |
1133 pass | |
1134 | |
1135 | |
1136 class InMemoryRegistry: | |
1137 """A simplistic registry for a specific domain.""" | |
1138 | |
1139 implements(IRegistry, ILocator) | |
1140 | |
1141 def __init__(self, domain): | |
1142 self.domain = domain # the domain we handle registration for | |
1143 self.users = {} # map username to (IDelayedCall for expiry, address URI) | |
1144 | |
1145 def getAddress(self, userURI): | |
1146 if userURI.host != self.domain: | |
1147 return defer.fail(LookupError("unknown domain")) | |
1148 if self.users.has_key(userURI.username): | |
1149 dc, url = self.users[userURI.username] | |
1150 return defer.succeed(url) | |
1151 else: | |
1152 return defer.fail(LookupError("no such user")) | |
1153 | |
1154 def getRegistrationInfo(self, userURI): | |
1155 if userURI.host != self.domain: | |
1156 return defer.fail(LookupError("unknown domain")) | |
1157 if self.users.has_key(userURI.username): | |
1158 dc, url = self.users[userURI.username] | |
1159 return defer.succeed(Registration(int(dc.getTime() - time.time()), u
rl)) | |
1160 else: | |
1161 return defer.fail(LookupError("no such user")) | |
1162 | |
1163 def _expireRegistration(self, username): | |
1164 try: | |
1165 dc, url = self.users[username] | |
1166 except KeyError: | |
1167 return defer.fail(LookupError("no such user")) | |
1168 else: | |
1169 dc.cancel() | |
1170 del self.users[username] | |
1171 return defer.succeed(Registration(0, url)) | |
1172 | |
1173 def registerAddress(self, domainURL, logicalURL, physicalURL): | |
1174 if domainURL.host != self.domain: | |
1175 log.msg("Registration for domain we don't handle.") | |
1176 return defer.fail(RegistrationError(404)) | |
1177 if logicalURL.host != self.domain: | |
1178 log.msg("Registration for domain we don't handle.") | |
1179 return defer.fail(RegistrationError(404)) | |
1180 if self.users.has_key(logicalURL.username): | |
1181 dc, old = self.users[logicalURL.username] | |
1182 dc.reset(3600) | |
1183 else: | |
1184 dc = reactor.callLater(3600, self._expireRegistration, logicalURL.us
ername) | |
1185 log.msg("Registered %s at %s" % (logicalURL.toString(), physicalURL.toSt
ring())) | |
1186 self.users[logicalURL.username] = (dc, physicalURL) | |
1187 return defer.succeed(Registration(int(dc.getTime() - time.time()), physi
calURL)) | |
1188 | |
1189 def unregisterAddress(self, domainURL, logicalURL, physicalURL): | |
1190 return self._expireRegistration(logicalURL.username) | |
OLD | NEW |