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