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.twisted import reflect as freflect
43 from flumotion.twisted import credentials as fcredentials
44
45
46
47
48
49
50
51
52
53
54
55
56
57
59 """
60 I am an extended Perspective Broker client factory using generic
61 keycards for login.
62
63
64 @ivar keycard: the keycard used last for logging in; set after
65 self.login has completed
66 @type keycard: L{keycards.Keycard}
67 @ivar medium: the client-side referenceable for the PB server
68 to call on, and for the client to call to the
69 PB server
70 @type medium: L{flumotion.common.medium.BaseMedium}
71 @ivar perspectiveInterface: the interface we want to request a perspective
72 for
73 @type perspectiveInterface: subclass of
74 L{flumotion.common.interfaces.IMedium}
75 """
76 logCategory = "FPBClientFactory"
77 keycard = None
78 medium = None
79 perspectiveInterface = None
80 _fpbconnector = None
81
82
86
87
95
97 """
98 Ask the remote PB server for all the keycard interfaces it supports.
99
100 @rtype: L{twisted.internet.defer.Deferred} returning list of str
101 """
102 def getRootObjectCb(root):
103 return root.callRemote('getKeycardClasses')
104
105 d = self.getRootObject()
106 d.addCallback(getRootObjectCb)
107 return d
108
109 - def login(self, authenticator):
136
137 def issueCb(keycard):
138 self.keycard = keycard
139 self.debug('using keycard: %r' % self.keycard)
140 return self.keycard
141
142 d = self.getKeycardClasses()
143 d.addCallback(getKeycardClassesCb)
144 d.addCallback(issueCb)
145 d.addCallback(lambda r: self.getRootObject())
146 d.addCallback(self._cbSendKeycard, authenticator, self.medium,
147 interfaces)
148 return d
149
150
151 - def _cbSendUsername(self, root, username, password, avatarId, client, interfaces):
152 self.warning("you really want to use cbSendKeycard")
153
154
155 - def _cbSendKeycard(self, root, authenticator, client, interfaces, count=0):
156 self.log("_cbSendKeycard(root=%r, authenticator=%r, client=%r, "
157 "interfaces=%r, count=%d", root, authenticator, client,
158 interfaces, count)
159 count = count + 1
160 d = root.callRemote("login", self.keycard, client, *interfaces)
161 return d.addCallback(self._cbLoginCallback, root, authenticator, client,
162 interfaces, count)
163
164
165 - def _cbLoginCallback(self, result, root, authenticator, client, interfaces,
166 count):
167 if count > 5:
168
169 self.warning('Too many recursions, internal error.')
170 self.log("FPBClientFactory(): result %r" % result)
171
172 if isinstance(result, pb.RemoteReference):
173
174 self.debug('login successful, returning %r', result)
175 return result
176
177
178 keycard = result
179 if not keycard.state == keycards.AUTHENTICATED:
180 self.log("FPBClientFactory(): requester needs to resend %r",
181 keycard)
182 d = authenticator.respond(keycard)
183 def _loginAgainCb(keycard):
184 d = root.callRemote("login", keycard, client, *interfaces)
185 return d.addCallback(self._cbLoginCallback, root, authenticator,
186 client, interfaces, count)
187 d.addCallback(_loginAgainCb)
188 return d
189
190 self.debug("FPBClientFactory(): authenticated %r" % keycard)
191 return keycard
192
195 """
196 Reconnecting client factory for normal PB brokers.
197
198 Users of this factory call startLogin to start logging in, and should
199 override getLoginDeferred to get the deferred returned from the PB server
200 for each login attempt.
201 """
202
204 pb.PBClientFactory.__init__(self)
205 self._doingLogin = False
206
208 log.msg("connection failed to %s, reason %r" % (
209 connector.getDestination(), reason))
210 pb.PBClientFactory.clientConnectionFailed(self, connector, reason)
211 RCF = protocol.ReconnectingClientFactory
212 RCF.clientConnectionFailed(self, connector, reason)
213
215 log.msg("connection lost to %s, reason %r" % (
216 connector.getDestination(), reason))
217 pb.PBClientFactory.clientConnectionLost(self, connector, reason,
218 reconnecting=True)
219 RCF = protocol.ReconnectingClientFactory
220 RCF.clientConnectionLost(self, connector, reason)
221
229
231 self._credentials = credentials
232 self._client = client
233
234 self._doingLogin = True
235
236
238 """
239 The deferred from login is now available.
240 """
241 raise NotImplementedError
242
245 """
246 Reconnecting client factory for FPB brokers (using keycards for login).
247
248 Users of this factory call startLogin to start logging in.
249 Override getLoginDeferred to get a handle to the deferred returned
250 from the PB server.
251 """
252
257
259 log.msg("connection failed to %s, reason %r" % (
260 connector.getDestination(), reason))
261 FPBClientFactory.clientConnectionFailed(self, connector, reason)
262 RCF = protocol.ReconnectingClientFactory
263 RCF.clientConnectionFailed(self, connector, reason)
264 if self.continueTrying:
265 self.debug("will try reconnect in %f seconds", self.delay)
266 else:
267 self.debug("not trying to reconnect")
268
276
284
285
286
288 assert not isinstance(authenticator, keycards.Keycard)
289 self._authenticator = authenticator
290 self._doingLogin = True
291
292
294 """
295 The deferred from login is now available.
296 """
297 raise NotImplementedError
298
299
300
301
302
303
304
306 """
307 Root object, used to login to bouncer.
308 """
309
310 implements(flavors.IPBRoot)
311
313 """
314 @type bouncerPortal: L{flumotion.twisted.portal.BouncerPortal}
315 """
316 self.bouncerPortal = bouncerPortal
317
320
322
323 logCategory = "_BouncerWrapper"
324
325 - def __init__(self, bouncerPortal, broker):
326 self.bouncerPortal = bouncerPortal
327 self.broker = broker
328
330 """
331 @returns: the fully-qualified class names of supported keycard
332 interfaces
333 @rtype: L{twisted.internet.defer.Deferred} firing list of str
334 """
335 return self.bouncerPortal.getKeycardClasses()
336
338 """
339 Start of keycard login.
340
341 @param interfaces: list of fully qualified names of interface objects
342
343 @returns: one of
344 - a L{flumotion.common.keycards.Keycard} when more steps
345 need to be performed
346 - a L{twisted.spread.pb.AsReferenceable} when authentication
347 has succeeded, which will turn into a
348 L{twisted.spread.pb.RemoteReference} on the client side
349 - a L{flumotion.common.errors.NotAuthenticatedError} when
350 authentication is denied
351 """
352 def loginResponse(result):
353 self.log("loginResponse: result=%r", result)
354
355 if isinstance(result, keycards.Keycard):
356 return result
357 else:
358
359 interface, perspective, logout = result
360 self.broker.notifyOnDisconnect(logout)
361 return pb.AsReferenceable(perspective, "perspective")
362
363
364 self.log("remote_login(keycard=%s, *interfaces=%r" % (keycard, interfaces))
365 interfaces = [freflect.namedAny(interface) for interface in interfaces]
366 d = self.bouncerPortal.login(keycard, mind, *interfaces)
367 d.addCallback(loginResponse)
368 return d
369
371 """
372 I am an object used by FPB clients to create keycards for me
373 and respond to challenges.
374
375 I encapsulate keycard-related data, plus secrets which are used locally
376 and not put on the keycard.
377
378 I can be serialized over PB connections to a RemoteReference and then
379 adapted with RemoteAuthenticator to present the same interface.
380
381 @cvar username: a username to log in with
382 @type username: str
383 @cvar password: a password to log in with
384 @type password: str
385 @cvar address: an address to log in from
386 @type address: str
387 @cvar avatarId: the avatarId we want to request from the PB server
388 @type avatarId: str
389 """
390 logCategory = "authenticator"
391
392 avatarId = None
393
394 username = None
395 password = None
396 address = None
397 ttl = 30
398
399
401 for key in kwargs:
402 setattr(self, key, kwargs[key])
403
404 - def issue(self, keycardClasses):
445
446
450
451
454
457
459 """
460 Respond to a challenge on the given keycard, based on the secrets
461 we have.
462
463 @param keycard: the keycard with the challenge to respond to
464 @type keycard: L{keycards.Keycard}
465
466 @rtype: L{twisted.internet.defer.Deferred} firing a {keycards.Keycard}
467 @returns: a deferred firing the keycard with a response set
468 """
469 self.debug('responding to challenge on keycard %r' % keycard)
470 methodName = "respond_%s" % keycard.__class__.__name__
471 method = getattr(self, methodName)
472 return defer.succeed(method(keycard))
473
478
483
484
487
490
492 """
493 I am an adapter for a pb.RemoteReference to present the same interface
494 as L{Authenticator}
495 """
496
497 avatarId = None
498 username = None
499 password = None
500
502 self._remote = remoteReference
503
504 - def copy(self, avatarId=None):
508
509 - def issue(self, interfaces):
513
514 d = self._remote.callRemote('issue', interfaces)
515 d.addCallback(issueCb)
516 return d
517
520
521
523 """
524 @cvar remoteLogName: name to use to log the other side of the connection
525 @type remoteLogName: str
526 """
527 logCategory = 'referenceable'
528 remoteLogName = 'remote'
529
530
531
533 args = broker.unserialize(args)
534 kwargs = broker.unserialize(kwargs)
535 method = getattr(self, "remote_%s" % message, None)
536 if method is None:
537 raise pb.NoSuchMethod("No such method: remote_%s" % (message,))
538
539 level = flog.DEBUG
540 if message == 'ping': level = flog.LOG
541
542 debugClass = self.logCategory.upper()
543
544
545 startArgs = [self.remoteLogName, debugClass, message]
546 format, debugArgs = flog.getFormatArgs(
547 '%s --> %s: remote_%s(', startArgs,
548 ')', (), args, kwargs)
549
550 logKwArgs = self.doLog(level, method, format, *debugArgs)
551
552
553 d = defer.maybeDeferred(method, *args, **kwargs)
554
555
556 def callback(result):
557 format, debugArgs = flog.getFormatArgs(
558 '%s <-- %s: remote_%s(', startArgs,
559 '): %r', (flog.ellipsize(result), ), args, kwargs)
560 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
561 return result
562 def errback(failure):
563 format, debugArgs = flog.getFormatArgs(
564 '%s <-- %s: remote_%s(', startArgs,
565 '): failure %r', (failure, ), args, kwargs)
566 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
567 return failure
568
569 d.addCallbacks(callback, errback)
570 return broker.serialize(d, self.perspective)
571
572 -class Avatar(pb.Avatar, flog.Loggable):
573 """
574 @cvar remoteLogName: name to use to log the other side of the connection
575 @type remoteLogName: str
576 """
577 logCategory = 'avatar'
578 remoteLogName = 'remote'
579
585
586
588 args = broker.unserialize(args)
589 kwargs = broker.unserialize(kwargs)
590 method = getattr(self, "perspective_%s" % message, None)
591 if method is None:
592 raise pb.NoSuchMethod("No such method: perspective_%s" % (message,))
593
594 level = flog.DEBUG
595 if message == 'ping': level = flog.LOG
596 debugClass = self.logCategory.upper()
597 startArgs = [self.remoteLogName, debugClass, message]
598 format, debugArgs = flog.getFormatArgs(
599 '%s --> %s: perspective_%s(', startArgs,
600 ')', (), args, kwargs)
601
602 logKwArgs = self.doLog(level, method, format, *debugArgs)
603
604
605 d = defer.maybeDeferred(method, *args, **kwargs)
606
607
608 def callback(result):
609 format, debugArgs = flog.getFormatArgs(
610 '%s <-- %s: perspective_%s(', startArgs,
611 '): %r', (flog.ellipsize(result), ), args, kwargs)
612 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
613 return result
614 def errback(failure):
615 format, debugArgs = flog.getFormatArgs(
616 '%s <-- %s: perspective_%s(', startArgs,
617 '): failure %r', (failure, ), args, kwargs)
618 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
619 return failure
620
621 d.addCallbacks(callback, errback)
622
623 return broker.serialize(d, self, method, args, kwargs)
624
626 """
627 Tell the avatar that the given mind has been attached.
628 This gives the avatar a way to call remotely to the client that
629 requested this avatar.
630
631 It is best to call setMind() from within the avatar's __init__
632 method. Some old code still does this via a callLater, however.
633
634 @type mind: L{twisted.spread.pb.RemoteReference}
635 """
636 self.mind = mind
637 def nullMind(x):
638 self.debug('%r: disconnected from %r' % (self, self.mind))
639 self.mind = None
640 self.mind.notifyOnDisconnect(nullMind)
641
642 transport = self.mind.broker.transport
643 tarzan = transport.getHost()
644 jane = transport.getPeer()
645 if tarzan and jane:
646 self.debug("PB client connection seen by me is from me %s to %s" % (
647 common.addressGetHost(tarzan),
648 common.addressGetHost(jane)))
649 self.log('Client attached is mind %s', mind)
650
653 """
654 Call the given remote method, and log calling and returning nicely.
655
656 @param level: the level we should log at (log.DEBUG, log.INFO, etc)
657 @type level: int
658 @param stackDepth: the number of stack frames to go back to get
659 file and line information, negative or zero.
660 @type stackDepth: non-positive int
661 @param name: name of the remote method
662 @type name: str
663 """
664 if level is not None:
665 debugClass = str(self.__class__).split(".")[-1].upper()
666 startArgs = [self.remoteLogName, debugClass, name]
667 format, debugArgs = flog.getFormatArgs(
668 '%s --> %s: callRemote(%s, ', startArgs,
669 ')', (), args, kwargs)
670 logKwArgs = self.doLog(level, stackDepth - 1, format,
671 *debugArgs)
672
673 if not self.mind:
674 self.warning('Tried to mindCallRemote(%s), but we are '
675 'disconnected', name)
676 return defer.fail(errors.NotConnectedError())
677
678 def callback(result):
679 format, debugArgs = flog.getFormatArgs(
680 '%s <-- %s: callRemote(%s, ', startArgs,
681 '): %r', (flog.ellipsize(result), ), args, kwargs)
682 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
683 return result
684
685 def errback(failure):
686 format, debugArgs = flog.getFormatArgs(
687 '%s <-- %s: callRemote(%s', startArgs,
688 '): %r', (failure, ), args, kwargs)
689 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
690 return failure
691
692 d = self.mind.callRemote(name, *args, **kwargs)
693 if level is not None:
694 d.addCallbacks(callback, errback)
695 return d
696
698 """
699 Call the given remote method, and log calling and returning nicely.
700
701 @param name: name of the remote method
702 @type name: str
703 """
704 return self.mindCallRemoteLogging(flog.DEBUG, -1, name, *args,
705 **kwargs)
706
708 """
709 Disconnect the remote PB client. If we are already disconnected,
710 do nothing.
711 """
712 if self.mind:
713 return self.mind.broker.transport.loseConnection()
714
716 _pingCheckInterval = configure.heartbeatInterval * 2.5
717
719 self._lastPing = time.time()
720 return defer.succeed(True)
721
726
736
738 if self._pingCheckDC:
739 self._pingCheckDC.cancel()
740 self._pingCheckDC = None
741
749 self.mind.notifyOnDisconnect(stopPingCheckingCb)
750
751
752 def _disconnect():
753 if self.mind:
754 self.mind.broker.transport.loseConnection()
755 self.startPingChecking(_disconnect)
756