1
# -*- test-case-name: twisted.test.test_sip -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
"""Session Initialization Protocol.
9
Documented in RFC 2543.
19
from zope.interface import implements, Interface
22
from twisted.python import log, util
23
from twisted.internet import protocol, defer, reactor
25
from twisted import cred
26
import twisted.cred.credentials
27
import twisted.cred.error
30
from twisted.protocols import basic
34
# SIP headers have short forms
35
shortHeaders = {"call-id": "i",
37
"content-encoding": "e",
38
"content-length": "l",
47
for k, v in shortHeaders.items():
54
181: "Call Is Being Forwarded",
56
183: "Session Progress",
60
300: "Multiple Choices",
61
301: "Moved Permanently",
62
302: "Moved Temporarily",
65
380: "Alternative Service",
69
402: "Payment Required",
72
405: "Method Not Allowed",
73
406: "Not Acceptable",
74
407: "Proxy Authentication Required",
75
408: "Request Timeout",
76
409: "Conflict", # Not in RFC3261
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",
84
421: "Extension Required",
85
423: "Interval Too Brief",
86
480: "Temporarily Unavailable",
87
481: "Call/Transaction Does Not Exist",
90
484: "Address Incomplete",
93
487: "Request Terminated",
94
488: "Not Acceptable Here",
95
491: "Request Pending",
96
493: "Undecipherable",
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",
106
600: "Busy Everywhere",
108
604: "Does not exist anywhere",
109
606: "Not Acceptable",
114
'call-id': 'Call-ID',
115
'www-authenticate': 'WWW-Authenticate',
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('-')])
123
if s[0] == s[-1] == '"':
136
m.update(pszUserName)
140
m.update(pszPassword)
142
if pszAlg == "md5-sess":
150
return HA1.encode('hex')
152
def DigestCalcResponse(
165
m.update(pszDigestUri)
166
if pszQop == "auth-int":
169
HA2 = m.digest().encode('hex')
176
if pszNonceCount and pszCNonce: # pszQop:
177
m.update(pszNonceCount)
184
hash = m.digest().encode('hex')
188
"""A SIP Via header."""
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
197
self.received = received
203
s = "SIP/2.0/%s %s:%s" % (self.transport, self.host, self.port)
206
for n in "ttl", "branch", "maddr", "received", "rport":
207
value = getattr(self, n)
211
s += ";%s=%s" % (n, value)
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)
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
227
host, port = by.split(":")
228
result["port"] = int(port)
229
result["host"] = host
233
# it's the comment-striping dance!
234
p = p.strip().split(" ", 1)
236
p, comment = p[0], ""
240
result["hidden"] = True
242
parts = p.split("=", 1)
244
name, value = parts[0], True
247
if name in ("rport", "ttl"):
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
261
self.password = password
263
self.transport = transport
264
self.usertype = usertype
276
self.headers = headers
281
if self.username != None:
283
if self.password != None:
284
w(":%s" % self.password)
287
if self.port != None:
289
if self.usertype != None:
290
w(";user=%s" % self.usertype)
291
for n in ("transport", "ttl", "maddr", "method", "tag"):
299
w("&".join([("%s=%s" % (specialCases.get(h) or dashCapitalize(h), v)) for (h, v) in self.headers.items()]))
303
return self.toString()
306
return '<URL %s:%s@%s:%r/%s>' % (self.username, self.password, self.host, self.port, self.transport)
309
def parseURL(url, host=None, port=None):
310
"""Return string into URL object.
312
URIs are of of form 'sip:user@example.com'.
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]
326
d["username"] = upparts[0]
327
d["password"] = upparts[1]
329
hostport = udparts[0]
330
hpparts = hostport.split(":", 1)
331
if len(hpparts) == 1:
332
d["host"] = hpparts[0]
334
d["host"] = hpparts[0]
335
d["port"] = int(hpparts[1])
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("=")
349
d.setdefault("other", []).append(p)
353
d["usertype"] = value
354
elif name in ("transport", "ttl", "maddr", "method", "tag"):
359
d.setdefault("other", []).append(p)
363
def cleanRequestURL(url):
364
"""Clean a URL from a Request line."""
371
def parseAddress(address, host=None, port=None, clean=0):
372
"""Return (name, uri, params) for From/To/Contact header.
374
@param clean: remove unnecessary info, usually for From and To headers.
376
address = address.strip()
377
# simple 'sip:foo' case
378
if address.startswith("sip:"):
379
return "", parseURL(address, host=host, port=port), {}
381
name, url = address.split("<", 1)
383
if name.startswith('"'):
385
if name.endswith('"'):
387
url, paramstring = url.split(">", 1)
388
url = parseURL(url, host=host, port=port)
389
paramstring = paramstring.strip()
391
for l in paramstring.split(";"):
402
return name, url, params
405
class SIPError(Exception):
406
def __init__(self, code, phrase=None):
408
phrase = statusCodes[code]
409
Exception.__init__(self, "SIP error (%d): %s" % (code, phrase))
414
class RegistrationError(SIPError):
415
"""Registration was not possible."""
424
self.headers = util.OrderedDict() # map name to list of values
428
def addHeader(self, name, value):
430
name = longHeaders.get(name, name)
431
if name == "content-length":
432
self.length = int(value)
433
self.headers.setdefault(name,[]).append(value)
435
def bodyDataReceived(self, data):
438
def creationFinished(self):
439
if (self.length != None) and (self.length != len(self.body)):
440
raise ValueError, "wrong body length"
444
s = "%s\r\n" % self._getHeaderLine()
445
for n, vs in self.headers.items():
447
s += "%s: %s\r\n" % (specialCases.get(n) or dashCapitalize(n), v)
452
def _getHeaderLine(self):
453
raise NotImplementedError
456
class Request(Message):
457
"""A Request for a URI"""
460
def __init__(self, method, uri, version="SIP/2.0"):
461
Message.__init__(self)
463
if isinstance(uri, URL):
466
self.uri = parseURL(uri)
467
cleanRequestURL(self.uri)
470
return "<SIP Request %d:%s %s>" % (id(self), self.method, self.uri.toString())
472
def _getHeaderLine(self):
473
return "%s %s SIP/2.0" % (self.method, self.uri.toString())
476
class Response(Message):
477
"""A Response to a URI Request"""
479
def __init__(self, code, phrase=None, version="SIP/2.0"):
480
Message.__init__(self)
483
phrase = statusCodes[code]
487
return "<SIP Response %d:%s>" % (id(self), self.code)
489
def _getHeaderLine(self):
490
return "SIP/2.0 %s %s" % (self.code, self.phrase)
493
class MessagesParser(basic.LineReceiver):
494
"""A SIP messages parser.
496
Expects dataReceived, dataDone repeatedly,
497
in that order. Shouldn't be connected to actual transport.
503
state = "firstline" # or "headers", "body" or "invalid"
507
def __init__(self, messageReceivedCallback):
508
self.messageReceived = messageReceivedCallback
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
516
self.setLineMode(remainingData)
518
def invalidMessage(self):
519
self.state = "invalid"
523
# clear out any buffered data that may be hanging around
524
self.clearLineBuffer()
525
if self.state == "firstline":
527
if self.state != "body":
530
if self.length == None:
531
# no content-length header, so end of data signals message done
533
elif self.length < self.bodyReceived:
534
# aborted in the middle
537
# we have enough data and message wasn't finished? something is wrong
538
raise RuntimeError, "this should never happen"
540
def dataReceived(self, data):
542
basic.LineReceiver.dataReceived(self, data)
545
self.invalidMessage()
547
def handleFirstLine(self, line):
548
"""Expected to create self.message."""
549
raise NotImplementedError
551
def lineLengthExceeded(self, line):
552
self.invalidMessage()
554
def lineReceived(self, line):
555
if self.state == "firstline":
556
while line.startswith("\n") or line.startswith("\r"):
561
a, b, c = line.split(" ", 2)
563
self.invalidMessage()
565
if a == "SIP/2.0" and self.acceptResponses:
570
self.invalidMessage()
572
self.message = Response(code, c)
573
elif c == "SIP/2.0" and self.acceptRequests:
574
self.message = Request(a, b)
576
self.invalidMessage()
578
self.state = "headers"
581
assert self.state == "headers"
583
# XXX support multi-line headers
585
name, value = line.split(":", 1)
587
self.invalidMessage()
589
self.message.addHeader(name, value.lstrip())
590
if name.lower() == "content-length":
592
self.length = int(value.lstrip())
594
self.invalidMessage()
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.
606
def messageDone(self, remainingData=""):
607
assert self.state == "body"
608
self.message.creationFinished()
609
self.messageReceived(self.message)
610
self.reset(remainingData)
612
def rawDataReceived(self, data):
613
assert self.state in ("body", "invalid")
614
if self.state == "invalid":
616
if self.length == None:
617
self.message.bodyDataReceived(data)
620
expectedLen = self.length - self.bodyReceived
621
if dataLen > expectedLen:
622
self.message.bodyDataReceived(data[:expectedLen])
623
self.messageDone(data[expectedLen:])
626
self.bodyReceived += dataLen
627
self.message.bodyDataReceived(data)
628
if self.bodyReceived == self.length:
632
class Base(protocol.DatagramProtocol):
633
"""Base class for SIP clients and servers."""
640
self.parser = MessagesParser(self.addMessage)
642
def addMessage(self, msg):
643
self.messages.append(msg)
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)
651
log.msg("Received %r from %r" % (m.toString(), addr))
652
if isinstance(m, Request):
653
self.handle_request(m, addr)
655
self.handle_response(m, addr)
656
self.messages[:] = []
658
def _fixupNAT(self, message, (srcHost, srcPort)):
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()
671
def deliverResponse(self, responseMessage):
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)
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, [])[:]
690
def sendMessage(self, destURL, message):
693
@param destURL: C{URL}. This should be a *physical* URL, not a logical one.
694
@param message: The message to send.
696
if destURL.transport not in ("udp", None):
697
raise RuntimeError, "only UDP currently supported"
699
log.msg("Sending %r to %r" % (message.toString(), destURL))
700
self.transport.write(message.toString(), (destURL.host, destURL.port or self.PORT))
702
def handle_request(self, message, addr):
703
"""Override to define behavior for requests received
705
@type message: C{Message}
708
raise NotImplementedError
710
def handle_response(self, message, addr):
711
"""Override to define behavior for responses received.
713
@type message: C{Message}
716
raise NotImplementedError
719
class IContact(Interface):
720
"""A user of a registrar or proxy"""
724
def __init__(self, secondsToExpiry, contactURL):
725
self.secondsToExpiry = secondsToExpiry
726
self.contactURL = contactURL
728
class IRegistry(Interface):
729
"""Allows registration of logical->physical URL mapping."""
731
def registerAddress(domainURL, logicalURL, physicalURL):
732
"""Register the physical address of a logical URL.
734
@return: Deferred of C{Registration} or failure with RegistrationError.
737
def unregisterAddress(domainURL, logicalURL, physicalURL):
738
"""Unregister the physical address of a logical URL.
740
@return: Deferred of C{Registration} or failure with RegistrationError.
743
def getRegistrationInfo(logicalURL):
744
"""Get registration info for logical URL.
746
@return: Deferred of C{Registration} object or failure of LookupError.
750
class ILocator(Interface):
751
"""Allow looking up physical address for logical URL."""
753
def getAddress(logicalURL):
754
"""Return physical URL of server for logical URL of user.
756
@param logicalURL: a logical C{URL}.
757
@return: Deferred which becomes URL or fails with LookupError.
766
locator = None # object implementing ILocator
768
def __init__(self, host=None, port=PORT):
769
"""Create new instance.
771
@param host: our hostname/IP as set in Via headers.
772
@param port: our port as set in Via headers.
774
self.host = host or socket.getfqdn()
779
"""Return value of Via header for this proxy."""
780
return Via(host=self.host, port=self.port)
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)
787
f = self.handle_request_default
791
self.deliverResponse(self.responseFromRequest(e.code, message))
794
self.deliverResponse(self.responseFromRequest(500, message))
797
d.addErrback(lambda e:
798
self.deliverResponse(self.responseFromRequest(e.code, message))
801
def handle_request_default(self, message, (srcHost, srcPort)):
802
"""Default request handler.
804
Default behaviour for OPTIONS and unknown methods for proxies
805
is to forward message on to the client.
807
Since at the moment we are stateless proxy, thats basically
810
def _mungContactHeader(uri, message):
811
message.headers['contact'][0] = uri.toString()
812
return self.sendMessage(uri, message)
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.")
820
message.headers["via"].insert(0, viaHeader.toString())
821
name, uri, tags = parseAddress(message.headers["to"][0], clean=1)
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)
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))
833
def deliverResponse(self, responseMessage):
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
842
destAddr = URL(host=host, port=port)
843
self.sendMessage(destAddr, responseMessage)
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, [])[:]
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")
861
del message.headers["via"][0]
862
if not message.headers["via"]:
863
# this message is addressed to us
864
self.gotResponse(message, addr)
866
self.deliverResponse(message)
868
def gotResponse(self, message, addr):
869
"""Called with responses that are addressed at this server."""
872
class IAuthorizer(Interface):
873
def getChallenge(peer):
874
"""Generate a challenge the client may respond to.
877
@param peer: The client's address
880
@return: The challenge string
883
def decode(response):
884
"""Create a credentials object from the given response.
886
@type response: C{str}
889
class BasicAuthorizer:
890
"""Authorizer for insecure Basic (base64-encoded plaintext) authentication.
892
This form of authentication is broken and insecure. Do not use it.
895
implements(IAuthorizer)
897
def getChallenge(self, peer):
900
def decode(self, response):
901
# At least one SIP client improperly pads its Base64 encoded messages
904
creds = (response + ('=' * i)).decode('base64')
912
p = creds.split(':', 1)
914
return cred.credentials.UsernamePassword(*p)
918
class DigestedCredentials(cred.credentials.UsernameHashedPassword):
919
"""Yet Another Simple Digest-MD5 authentication scheme"""
921
def __init__(self, username, fields, challenges):
922
self.username = username
924
self.challenges = challenges
926
def checkPassword(self, password):
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')
937
if opaque not in self.challenges:
939
del self.challenges[opaque]
941
user, domain = self.username.split('@', 1)
943
uri = 'sip:' + domain
945
expected = DigestCalcResponse(
946
DigestCalcHA1(algo, user, domain, password, nonce, cnonce),
947
nonce, nc, cnonce, qop, method, uri, None,
950
return expected == response
952
class DigestAuthorizer:
953
CHALLENGE_LIFETIME = 15
955
implements(IAuthorizer)
958
self.outstanding = {}
960
def generateNonce(self):
961
c = tuple([random.randrange(sys.maxint) for _ in range(3)])
965
def generateOpaque(self):
966
return str(random.randrange(sys.maxint))
968
def getChallenge(self, peer):
969
c = self.generateNonce()
970
o = self.generateOpaque()
971
self.outstanding[o] = c
975
'qop-options="auth"',
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]])
984
username = auth['username']
988
return DigestedCredentials(username, auth, self.outstanding)
993
class RegisterProxy(Proxy):
994
"""A proxy that allows registration for a specific domain.
996
Unregistered users won't be handled.
1001
registry = None # should implement IRegistry
1004
'digest': DigestAuthorizer(),
1007
def __init__(self, *args, **kw):
1008
Proxy.__init__(self, *args, **kw)
1009
self.liveChallenges = {}
1011
def handle_ACK_request(self, message, (host, port)):
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.
1019
def handle_REGISTER_request(self, message, (host, port)):
1020
"""Handle a registration request.
1022
Currently registration is not proxied.
1024
if self.portal is None:
1025
# There is no portal. Let anyone in.
1026
self.register(message, host, port)
1028
# There is a portal. Check for credentials.
1029
if not message.headers.has_key("authorization"):
1030
return self.unauthorized(message, host, port)
1032
return self.login(message, host, port)
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))
1039
value = '%s realm="%s"' % (scheme.title(), self.host)
1041
value = '%s %s,realm="%s"' % (scheme.title(), chal, self.host)
1042
m.headers.setdefault('www-authenticate', []).append(value)
1043
self.deliverResponse(m)
1046
def login(self, message, host, port):
1047
parts = message.headers['authorization'][0].split(None, 1)
1048
a = self.authorizers.get(parts[0].lower())
1051
c = a.decode(parts[1])
1056
self.deliverResponse(self.responseFromRequest(500, message))
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
1065
self.deliverResponse(self.responseFromRequest(501, message))
1067
def _cbLogin(self, (i, a, l), message, host, port):
1068
# It's stateless, matey. What a joke.
1069
self.register(message, host, port)
1071
def _ebLogin(self, failure, message, host, port):
1072
failure.trap(cred.error.UnauthorizedLogin)
1073
self.unauthorized(message, host, port)
1075
def register(self, message, host, port):
1076
"""Allow all users to register"""
1077
name, toURL, params = parseAddress(message.headers["to"][0], clean=1)
1079
if message.headers.has_key("contact"):
1080
contact = message.headers["contact"][0]
1082
if message.headers.get("expires", [None])[0] == "0":
1083
self.unregister(message, toURL, contact)
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)
1091
d = self.registry.getRegistrationInfo(toURL)
1092
d.addCallbacks(self._cbRegister, self._ebRegister,
1093
callbackArgs=(message,),
1094
errbackArgs=(message,)
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)
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
1110
def unregister(self, message, toURL, contact):
1112
expires = int(message.headers["expires"][0])
1114
self.deliverResponse(self.responseFromRequest(400, message))
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
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)
1132
def _ebUnregister(self, registration, message):
1136
class InMemoryRegistry:
1137
"""A simplistic registry for a specific domain."""
1139
implements(IRegistry, ILocator)
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)
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)
1152
return defer.fail(LookupError("no such user"))
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))
1161
return defer.fail(LookupError("no such user"))
1163
def _expireRegistration(self, username):
1165
dc, url = self.users[username]
1167
return defer.fail(LookupError("no such user"))
1170
del self.users[username]
1171
return defer.succeed(Registration(0, url))
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]
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))
1189
def unregisterAddress(self, domainURL, logicalURL, physicalURL):
1190
return self._expireRegistration(logicalURL.username)