~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/protocols/sip.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

Show diffs side-by-side

added added

removed removed

Lines of Context:
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.toString())
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 wrong
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 one.
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, message))
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, contactURL)
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.toString())
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()), url))
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.username)
1185
 
        log.msg("Registered %s at %s" % (logicalURL.toString(), physicalURL.toString()))
1186
 
        self.users[logicalURL.username] = (dc, physicalURL)
1187
 
        return defer.succeed(Registration(int(dc.getTime() - time.time()), physicalURL))
1188
 
 
1189
 
    def unregisterAddress(self, domainURL, logicalURL, physicalURL):
1190
 
        return self._expireRegistration(logicalURL.username)