1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """
19 Flumotion Perspective Broker using keycards
20
21 Inspired by L{twisted.spread.pb}
22 """
23
24 from twisted.cred.portal import IRealm, Portal
25 from twisted.internet import protocol, defer
26 from twisted.internet import error as terror
27 from twisted.python import log, reflect
28 from twisted.spread import pb, flavors
29 from twisted.spread.pb import PBClientFactory
30 from zope.interface import implements
31
32 from flumotion.configure import configure
33 from flumotion.common import keycards, errors
34 from flumotion.common import log as flog
35 from flumotion.common.netutils import addressGetHost
36 from flumotion.twisted import reflect as freflect
37 from flumotion.twisted.compat import reactor
38
39 __version__ = "$Rev$"
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
58 """
59 I am an extended Perspective Broker client factory using generic
60 keycards for login.
61
62
63 @ivar keycard: the keycard used last for logging in; set after
64 self.login has completed
65 @type keycard: L{keycards.Keycard}
66 @ivar medium: the client-side referenceable for the PB server
67 to call on, and for the client to call to the
68 PB server
69 @type medium: L{flumotion.common.medium.BaseMedium}
70 @ivar perspectiveInterface: the interface we want to request a perspective
71 for
72 @type perspectiveInterface: subclass of
73 L{flumotion.common.interfaces.IMedium}
74 """
75 logCategory = "FPBClientFactory"
76 keycard = None
77 medium = None
78 perspectiveInterface = None
79 _fpbconnector = None
80
81
82
86
87
88
96
98 """
99 Ask the remote PB server for all the keycard interfaces it supports.
100
101 @rtype: L{twisted.internet.defer.Deferred} returning list of str
102 """
103
104 def getRootObjectCb(root):
105 return root.callRemote('getKeycardClasses')
106
107 d = self.getRootObject()
108 d.addCallback(getRootObjectCb)
109 return d
110
111 - def login(self, authenticator):
139
140 def issueCb(keycard):
141 self.keycard = keycard
142 self.debug('using keycard: %r' % self.keycard)
143 return self.keycard
144
145 d = self.getKeycardClasses()
146 d.addCallback(getKeycardClassesCb)
147 d.addCallback(issueCb)
148 d.addCallback(lambda r: self.getRootObject())
149 d.addCallback(self._cbSendKeycard, authenticator, self.medium,
150 interfaces)
151 return d
152
153
154
155 - def _cbSendUsername(self, root, username, password,
156 avatarId, client, interfaces):
157 self.warning("you really want to use cbSendKeycard")
158
159 - def _cbSendKeycard(self, root, authenticator, client, interfaces, count=0):
160 self.log("_cbSendKeycard(root=%r, authenticator=%r, client=%r, "
161 "interfaces=%r, count=%d", root, authenticator, client,
162 interfaces, count)
163 count = count + 1
164 d = root.callRemote("login", self.keycard, client, *interfaces)
165 return d.addCallback(self._cbLoginCallback, root,
166 authenticator, client, interfaces, count)
167
168
169
170 - def _cbLoginCallback(self, result, root, authenticator, client, interfaces,
171 count):
172 if count > 5:
173
174 self.warning('Too many recursions, internal error.')
175 self.log("FPBClientFactory(): result %r" % result)
176
177 if isinstance(result, pb.RemoteReference):
178
179 self.debug('login successful, returning %r', result)
180 return result
181
182
183 keycard = result
184 if not keycard.state == keycards.AUTHENTICATED:
185 self.log("FPBClientFactory(): requester needs to resend %r",
186 keycard)
187 d = authenticator.respond(keycard)
188
189 def _loginAgainCb(keycard):
190 d = root.callRemote("login", keycard, client, *interfaces)
191 return d.addCallback(self._cbLoginCallback, root,
192 authenticator, client,
193 interfaces, count)
194 d.addCallback(_loginAgainCb)
195 return d
196
197 self.debug("FPBClientFactory(): authenticated %r" % keycard)
198 return keycard
199
200
203 """
204 Reconnecting client factory for normal PB brokers.
205
206 Users of this factory call startLogin to start logging in, and should
207 override getLoginDeferred to get the deferred returned from the PB server
208 for each login attempt.
209 """
210
212 pb.PBClientFactory.__init__(self)
213 self._doingLogin = False
214
221
223 log.msg("connection lost to %s, reason %r" % (
224 connector.getDestination(), reason))
225 pb.PBClientFactory.clientConnectionLost(self, connector, reason,
226 reconnecting=True)
227 RCF = protocol.ReconnectingClientFactory
228 RCF.clientConnectionLost(self, connector, reason)
229
237
239 self._credentials = credentials
240 self._client = client
241
242 self._doingLogin = True
243
244
245
247 """
248 The deferred from login is now available.
249 """
250 raise NotImplementedError
251
252
255 """
256 Reconnecting client factory for FPB brokers (using keycards for login).
257
258 Users of this factory call startLogin to start logging in.
259 Override getLoginDeferred to get a handle to the deferred returned
260 from the PB server.
261 """
262
267
278
286
294
295
296
297
299 assert not isinstance(authenticator, keycards.Keycard)
300 self._authenticator = authenticator
301 self._doingLogin = True
302
303
304
306 """
307 The deferred from login is now available.
308 """
309 raise NotImplementedError
310
311
312
313
314
315
316
317
318
320 """
321 Root object, used to login to bouncer.
322 """
323
324 implements(flavors.IPBRoot)
325
327 """
328 @type bouncerPortal: L{flumotion.twisted.portal.BouncerPortal}
329 """
330 self.bouncerPortal = bouncerPortal
331
334
335
337
338 logCategory = "_BouncerWrapper"
339
340 - def __init__(self, bouncerPortal, broker):
341 self.bouncerPortal = bouncerPortal
342 self.broker = broker
343
345 """
346 @returns: the fully-qualified class names of supported keycard
347 interfaces
348 @rtype: L{twisted.internet.defer.Deferred} firing list of str
349 """
350 return self.bouncerPortal.getKeycardClasses()
351
353 """
354 Start of keycard login.
355
356 @param interfaces: list of fully qualified names of interface objects
357
358 @returns: one of
359 - a L{flumotion.common.keycards.Keycard} when more steps
360 need to be performed
361 - a L{twisted.spread.pb.AsReferenceable} when authentication
362 has succeeded, which will turn into a
363 L{twisted.spread.pb.RemoteReference} on the client side
364 - a L{flumotion.common.errors.NotAuthenticatedError} when
365 authentication is denied
366 """
367
368 def loginResponse(result):
369 self.log("loginResponse: result=%r", result)
370
371 if isinstance(result, keycards.Keycard):
372 return result
373 else:
374
375 interface, perspective, logout = result
376 self.broker.notifyOnDisconnect(logout)
377 return pb.AsReferenceable(perspective, "perspective")
378
379
380 self.log("remote_login(keycard=%s, *interfaces=%r" % (
381 keycard, interfaces))
382 interfaces = [freflect.namedAny(interface) for interface in interfaces]
383 d = self.bouncerPortal.login(keycard, mind, *interfaces)
384 d.addCallback(loginResponse)
385 return d
386
387
389 """
390 I am an object used by FPB clients to create keycards for me
391 and respond to challenges.
392
393 I encapsulate keycard-related data, plus secrets which are used locally
394 and not put on the keycard.
395
396 I can be serialized over PB connections to a RemoteReference and then
397 adapted with RemoteAuthenticator to present the same interface.
398
399 @cvar username: a username to log in with
400 @type username: str
401 @cvar password: a password to log in with
402 @type password: str
403 @cvar address: an address to log in from
404 @type address: str
405 @cvar avatarId: the avatarId we want to request from the PB server
406 @type avatarId: str
407 """
408 logCategory = "authenticator"
409
410 avatarId = None
411
412 username = None
413 password = None
414 address = None
415 ttl = 30
416
417
419 for key in kwargs:
420 setattr(self, key, kwargs[key])
421
422 - def issue(self, keycardClasses):
463
464
465
469
475
476
479
482
484 """
485 Respond to a challenge on the given keycard, based on the secrets
486 we have.
487
488 @param keycard: the keycard with the challenge to respond to
489 @type keycard: L{keycards.Keycard}
490
491 @rtype: L{twisted.internet.defer.Deferred} firing
492 a {keycards.Keycard}
493 @returns: a deferred firing the keycard with a response set
494 """
495 self.debug('responding to challenge on keycard %r' % keycard)
496 methodName = "respond_%s" % keycard.__class__.__name__
497 method = getattr(self, methodName)
498 return defer.succeed(method(keycard))
499
504
509
510
511
514
517
518
520 """
521 I am an adapter for a pb.RemoteReference to present the same interface
522 as L{Authenticator}
523 """
524
525 avatarId = None
526 username = None
527 password = None
528
530 self._remote = remoteReference
531
532 - def copy(self, avatarId=None):
536
537 - def issue(self, interfaces):
542
543 d = self._remote.callRemote('issue', interfaces)
544 d.addCallback(issueCb)
545 return d
546
549
550
552 """
553 @cvar remoteLogName: name to use to log the other side of the connection
554 @type remoteLogName: str
555 """
556 logCategory = 'referenceable'
557 remoteLogName = 'remote'
558
559
560
561
563 args = broker.unserialize(args)
564 kwargs = broker.unserialize(kwargs)
565 method = getattr(self, "remote_%s" % message, None)
566 if method is None:
567 raise pb.NoSuchMethod("No such method: remote_%s" % (message, ))
568
569 level = flog.DEBUG
570 if message == 'ping':
571 level = flog.LOG
572
573 debugClass = self.logCategory.upper()
574
575
576 startArgs = [self.remoteLogName, debugClass, message]
577 formatString, debugArgs = flog.getFormatArgs(
578 '%s --> %s: remote_%s(', startArgs,
579 ')', (), args, kwargs)
580
581 logKwArgs = self.doLog(level, method, formatString, *debugArgs)
582
583
584 d = defer.maybeDeferred(method, *args, **kwargs)
585
586
587
588 def callback(result):
589 formatString, debugArgs = flog.getFormatArgs(
590 '%s <-- %s: remote_%s(', startArgs,
591 '): %r', (flog.ellipsize(result), ), args, kwargs)
592 self.doLog(level, -1, formatString, *debugArgs, **logKwArgs)
593 return result
594
595 def errback(failure):
596 formatString, debugArgs = flog.getFormatArgs(
597 '%s <-- %s: remote_%s(', startArgs,
598 '): failure %r', (failure, ), args, kwargs)
599 self.doLog(level, -1, formatString, *debugArgs, **logKwArgs)
600 return failure
601
602 d.addCallbacks(callback, errback)
603 return broker.serialize(d, self.perspective)
604
605
606 -class Avatar(pb.Avatar, flog.Loggable):
607 """
608 @cvar remoteLogName: name to use to log the other side of the connection
609 @type remoteLogName: str
610 """
611 logCategory = 'avatar'
612 remoteLogName = 'remote'
613
619
620
621
627
630 method = getattr(self, "perspective_%s" % message, None)
631 if method is None:
632 raise pb.NoSuchMethod("No such method: perspective_%s" % (
633 message, ))
634
635 level = flog.DEBUG
636 if message == 'ping':
637 level = flog.LOG
638 debugClass = self.logCategory.upper()
639 startArgs = [self.remoteLogName, debugClass, message]
640 formatString, debugArgs = flog.getFormatArgs(
641 '%s --> %s: perspective_%s(', startArgs,
642 ')', (), args, kwargs)
643
644 logKwArgs = self.doLog(level, method, formatString, *debugArgs)
645
646
647 d = defer.maybeDeferred(method, *args, **kwargs)
648
649
650
651 def callback(result):
652 formatString, debugArgs = flog.getFormatArgs(
653 '%s <-- %s: perspective_%s(', startArgs,
654 '): %r', (flog.ellipsize(result), ), args, kwargs)
655 self.doLog(level, -1, formatString, *debugArgs, **logKwArgs)
656 return result
657
658 def errback(failure):
659 formatString, debugArgs = flog.getFormatArgs(
660 '%s <-- %s: perspective_%s(', startArgs,
661 '): failure %r', (failure, ), args, kwargs)
662 self.doLog(level, -1, formatString, *debugArgs, **logKwArgs)
663 return failure
664
665 d.addCallbacks(callback, errback)
666
667 return broker.serialize(d, self, method, args, kwargs)
668
670 """
671 Tell the avatar that the given mind has been attached.
672 This gives the avatar a way to call remotely to the client that
673 requested this avatar.
674
675 It is best to call setMind() from within the avatar's __init__
676 method. Some old code still does this via a callLater, however.
677
678 @type mind: L{twisted.spread.pb.RemoteReference}
679 """
680 self.mind = mind
681
682 def nullMind(x):
683 self.debug('%r: disconnected from %r' % (self, self.mind))
684 self.mind = None
685 self.mind.notifyOnDisconnect(nullMind)
686
687 transport = self.mind.broker.transport
688 tarzan = transport.getHost()
689 jane = transport.getPeer()
690 if tarzan and jane:
691 self.debug(
692 "PB client connection seen by me is from me %s to %s" % (
693 addressGetHost(tarzan),
694 addressGetHost(jane)))
695 self.log('Client attached is mind %s', mind)
696
699 """
700 Call the given remote method, and log calling and returning nicely.
701
702 @param level: the level we should log at (log.DEBUG, log.INFO, etc)
703 @type level: int
704 @param stackDepth: the number of stack frames to go back to get
705 file and line information, negative or zero.
706 @type stackDepth: non-positive int
707 @param name: name of the remote method
708 @type name: str
709 """
710 if level is not None:
711 debugClass = str(self.__class__).split(".")[-1].upper()
712 startArgs = [self.remoteLogName, debugClass, name]
713 formatString, debugArgs = flog.getFormatArgs(
714 '%s --> %s: callRemote(%s, ', startArgs,
715 ')', (), args, kwargs)
716 logKwArgs = self.doLog(level, stackDepth - 1, formatString,
717 *debugArgs)
718
719 if not self.mind:
720 self.warning('Tried to mindCallRemote(%s), but we are '
721 'disconnected', name)
722 return defer.fail(errors.NotConnectedError())
723
724 def callback(result):
725 formatString, debugArgs = flog.getFormatArgs(
726 '%s <-- %s: callRemote(%s, ', startArgs,
727 '): %r', (flog.ellipsize(result), ), args, kwargs)
728 self.doLog(level, -1, formatString, *debugArgs, **logKwArgs)
729 return result
730
731 def errback(failure):
732 formatString, debugArgs = flog.getFormatArgs(
733 '%s <-- %s: callRemote(%s, ', startArgs,
734 '): %r', (failure, ), args, kwargs)
735 self.doLog(level, -1, formatString, *debugArgs, **logKwArgs)
736 return failure
737
738 d = self.mind.callRemote(name, *args, **kwargs)
739 if level is not None:
740 d.addCallbacks(callback, errback)
741 return d
742
744 """
745 Call the given remote method, and log calling and returning nicely.
746
747 @param name: name of the remote method
748 @type name: str
749 """
750 return self.mindCallRemoteLogging(flog.DEBUG, -1, name, *args,
751 **kwargs)
752
754 """
755 Disconnect the remote PB client. If we are already disconnected,
756 do nothing.
757 """
758 if self.mind:
759 return self.mind.broker.transport.loseConnection()
760
761
789
794
804
806 if self._pingCheckDC:
807 self._pingCheckDC.cancel()
808 self._pingCheckDC = None
809
810
811
812 self._pingCheckDisconnect = None
813
821 self.mind.notifyOnDisconnect(stopPingCheckingCb)
822
823
824
825 def _disconnect():
826 if self.mind:
827 self.mind.broker.transport.loseConnection()
828 self.startPingChecking(_disconnect)
829