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