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: 6693 $"
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
91
92
100
102 """
103 Ask the remote PB server for all the keycard interfaces it supports.
104
105 @rtype: L{twisted.internet.defer.Deferred} returning list of str
106 """
107 def getRootObjectCb(root):
108 return root.callRemote('getKeycardClasses')
109
110 d = self.getRootObject()
111 d.addCallback(getRootObjectCb)
112 return d
113
114 - def login(self, authenticator):
141
142 def issueCb(keycard):
143 self.keycard = keycard
144 self.debug('using keycard: %r' % self.keycard)
145 return self.keycard
146
147 d = self.getKeycardClasses()
148 d.addCallback(getKeycardClassesCb)
149 d.addCallback(issueCb)
150 d.addCallback(lambda r: self.getRootObject())
151 d.addCallback(self._cbSendKeycard, authenticator, self.medium,
152 interfaces)
153 return d
154
155
156 - def _cbSendUsername(self, root, username, password, avatarId, client, interfaces):
157 self.warning("you really want to use cbSendKeycard")
158
159
160 - def _cbSendKeycard(self, root, authenticator, client, interfaces, count=0):
161 self.log("_cbSendKeycard(root=%r, authenticator=%r, client=%r, "
162 "interfaces=%r, count=%d", root, authenticator, client,
163 interfaces, count)
164 count = count + 1
165 d = root.callRemote("login", self.keycard, client, *interfaces)
166 return d.addCallback(self._cbLoginCallback, root, authenticator, client,
167 interfaces, count)
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 def _loginAgainCb(keycard):
189 d = root.callRemote("login", keycard, client, *interfaces)
190 return d.addCallback(self._cbLoginCallback, root, authenticator,
191 client, interfaces, count)
192 d.addCallback(_loginAgainCb)
193 return d
194
195 self.debug("FPBClientFactory(): authenticated %r" % keycard)
196 return keycard
197
200 """
201 Reconnecting client factory for normal PB brokers.
202
203 Users of this factory call startLogin to start logging in, and should
204 override getLoginDeferred to get the deferred returned from the PB server
205 for each login attempt.
206 """
207
209 pb.PBClientFactory.__init__(self)
210 self._doingLogin = False
211
213 log.msg("connection failed to %s, reason %r" % (
214 connector.getDestination(), reason))
215 pb.PBClientFactory.clientConnectionFailed(self, connector, reason)
216 RCF = protocol.ReconnectingClientFactory
217 RCF.clientConnectionFailed(self, connector, reason)
218
220 log.msg("connection lost to %s, reason %r" % (
221 connector.getDestination(), reason))
222 pb.PBClientFactory.clientConnectionLost(self, connector, reason,
223 reconnecting=True)
224 RCF = protocol.ReconnectingClientFactory
225 RCF.clientConnectionLost(self, connector, reason)
226
234
236 self._credentials = credentials
237 self._client = client
238
239 self._doingLogin = True
240
241
243 """
244 The deferred from login is now available.
245 """
246 raise NotImplementedError
247
250 """
251 Reconnecting client factory for FPB brokers (using keycards for login).
252
253 Users of this factory call startLogin to start logging in.
254 Override getLoginDeferred to get a handle to the deferred returned
255 from the PB server.
256 """
257
262
264 log.msg("connection failed to %s, reason %r" % (
265 connector.getDestination(), reason))
266 FPBClientFactory.clientConnectionFailed(self, connector, reason)
267 RCF = protocol.ReconnectingClientFactory
268 RCF.clientConnectionFailed(self, connector, reason)
269 if self.continueTrying:
270 self.debug("will try reconnect in %f seconds", self.delay)
271 else:
272 self.debug("not trying to reconnect")
273
281
289
290
291
293 assert not isinstance(authenticator, keycards.Keycard)
294 self._authenticator = authenticator
295 self._doingLogin = True
296
297
299 """
300 The deferred from login is now available.
301 """
302 raise NotImplementedError
303
304
305
306
307
308
309
311 """
312 Root object, used to login to bouncer.
313 """
314
315 implements(flavors.IPBRoot)
316
318 """
319 @type bouncerPortal: L{flumotion.twisted.portal.BouncerPortal}
320 """
321 self.bouncerPortal = bouncerPortal
322
325
327
328 logCategory = "_BouncerWrapper"
329
330 - def __init__(self, bouncerPortal, broker):
331 self.bouncerPortal = bouncerPortal
332 self.broker = broker
333
335 """
336 @returns: the fully-qualified class names of supported keycard
337 interfaces
338 @rtype: L{twisted.internet.defer.Deferred} firing list of str
339 """
340 return self.bouncerPortal.getKeycardClasses()
341
343 """
344 Start of keycard login.
345
346 @param interfaces: list of fully qualified names of interface objects
347
348 @returns: one of
349 - a L{flumotion.common.keycards.Keycard} when more steps
350 need to be performed
351 - a L{twisted.spread.pb.AsReferenceable} when authentication
352 has succeeded, which will turn into a
353 L{twisted.spread.pb.RemoteReference} on the client side
354 - a L{flumotion.common.errors.NotAuthenticatedError} when
355 authentication is denied
356 """
357 def loginResponse(result):
358 self.log("loginResponse: result=%r", result)
359
360 if isinstance(result, keycards.Keycard):
361 return result
362 else:
363
364 interface, perspective, logout = result
365 self.broker.notifyOnDisconnect(logout)
366 return pb.AsReferenceable(perspective, "perspective")
367
368
369 self.log("remote_login(keycard=%s, *interfaces=%r" % (keycard, interfaces))
370 interfaces = [freflect.namedAny(interface) for interface in interfaces]
371 d = self.bouncerPortal.login(keycard, mind, *interfaces)
372 d.addCallback(loginResponse)
373 return d
374
376 """
377 I am an object used by FPB clients to create keycards for me
378 and respond to challenges.
379
380 I encapsulate keycard-related data, plus secrets which are used locally
381 and not put on the keycard.
382
383 I can be serialized over PB connections to a RemoteReference and then
384 adapted with RemoteAuthenticator to present the same interface.
385
386 @cvar username: a username to log in with
387 @type username: str
388 @cvar password: a password to log in with
389 @type password: str
390 @cvar address: an address to log in from
391 @type address: str
392 @cvar avatarId: the avatarId we want to request from the PB server
393 @type avatarId: str
394 """
395 logCategory = "authenticator"
396
397 avatarId = None
398
399 username = None
400 password = None
401 address = None
402 ttl = 30
403
404
406 for key in kwargs:
407 setattr(self, key, kwargs[key])
408
409 - def issue(self, keycardClasses):
450
451
455
456
459
462
464 """
465 Respond to a challenge on the given keycard, based on the secrets
466 we have.
467
468 @param keycard: the keycard with the challenge to respond to
469 @type keycard: L{keycards.Keycard}
470
471 @rtype: L{twisted.internet.defer.Deferred} firing a {keycards.Keycard}
472 @returns: a deferred firing the keycard with a response set
473 """
474 self.debug('responding to challenge on keycard %r' % keycard)
475 methodName = "respond_%s" % keycard.__class__.__name__
476 method = getattr(self, methodName)
477 return defer.succeed(method(keycard))
478
483
488
489
492
495
497 """
498 I am an adapter for a pb.RemoteReference to present the same interface
499 as L{Authenticator}
500 """
501
502 avatarId = None
503 username = None
504 password = None
505
507 self._remote = remoteReference
508
509 - def copy(self, avatarId=None):
513
514 - def issue(self, interfaces):
518
519 d = self._remote.callRemote('issue', interfaces)
520 d.addCallback(issueCb)
521 return d
522
525
526
528 """
529 @cvar remoteLogName: name to use to log the other side of the connection
530 @type remoteLogName: str
531 """
532 logCategory = 'referenceable'
533 remoteLogName = 'remote'
534
535
536
538 args = broker.unserialize(args)
539 kwargs = broker.unserialize(kwargs)
540 method = getattr(self, "remote_%s" % message, None)
541 if method is None:
542 raise pb.NoSuchMethod("No such method: remote_%s" % (message,))
543
544 level = flog.DEBUG
545 if message == 'ping': level = flog.LOG
546
547 debugClass = self.logCategory.upper()
548
549
550 startArgs = [self.remoteLogName, debugClass, message]
551 format, debugArgs = flog.getFormatArgs(
552 '%s --> %s: remote_%s(', startArgs,
553 ')', (), args, kwargs)
554
555 logKwArgs = self.doLog(level, method, format, *debugArgs)
556
557
558 d = defer.maybeDeferred(method, *args, **kwargs)
559
560
561 def callback(result):
562 format, debugArgs = flog.getFormatArgs(
563 '%s <-- %s: remote_%s(', startArgs,
564 '): %r', (flog.ellipsize(result),), args, kwargs)
565 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
566 return result
567 def errback(failure):
568 format, debugArgs = flog.getFormatArgs(
569 '%s <-- %s: remote_%s(', startArgs,
570 '): failure %r', (failure,), args, kwargs)
571 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
572 return failure
573
574 d.addCallbacks(callback, errback)
575 return broker.serialize(d, self.perspective)
576
577 -class Avatar(pb.Avatar, flog.Loggable):
578 """
579 @cvar remoteLogName: name to use to log the other side of the connection
580 @type remoteLogName: str
581 """
582 logCategory = 'avatar'
583 remoteLogName = 'remote'
584
590
591
597
600 method = getattr(self, "perspective_%s" % message, None)
601 if method is None:
602 raise pb.NoSuchMethod("No such method: perspective_%s" % (message,))
603
604 level = flog.DEBUG
605 if message == 'ping': level = flog.LOG
606 debugClass = self.logCategory.upper()
607 startArgs = [self.remoteLogName, debugClass, message]
608 format, debugArgs = flog.getFormatArgs(
609 '%s --> %s: perspective_%s(', startArgs,
610 ')', (), args, kwargs)
611
612 logKwArgs = self.doLog(level, method, format, *debugArgs)
613
614
615 d = defer.maybeDeferred(method, *args, **kwargs)
616
617
618 def callback(result):
619 format, debugArgs = flog.getFormatArgs(
620 '%s <-- %s: perspective_%s(', startArgs,
621 '): %r', (flog.ellipsize(result),), args, kwargs)
622 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
623 return result
624 def errback(failure):
625 format, debugArgs = flog.getFormatArgs(
626 '%s <-- %s: perspective_%s(', startArgs,
627 '): failure %r', (failure,), args, kwargs)
628 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
629 return failure
630
631 d.addCallbacks(callback, errback)
632
633 return broker.serialize(d, self, method, args, kwargs)
634
636 """
637 Tell the avatar that the given mind has been attached.
638 This gives the avatar a way to call remotely to the client that
639 requested this avatar.
640
641 It is best to call setMind() from within the avatar's __init__
642 method. Some old code still does this via a callLater, however.
643
644 @type mind: L{twisted.spread.pb.RemoteReference}
645 """
646 self.mind = mind
647 def nullMind(x):
648 self.debug('%r: disconnected from %r' % (self, self.mind))
649 self.mind = None
650 self.mind.notifyOnDisconnect(nullMind)
651
652 transport = self.mind.broker.transport
653 tarzan = transport.getHost()
654 jane = transport.getPeer()
655 if tarzan and jane:
656 self.debug("PB client connection seen by me is from me %s to %s" % (
657 addressGetHost(tarzan),
658 addressGetHost(jane)))
659 self.log('Client attached is mind %s', mind)
660
663 """
664 Call the given remote method, and log calling and returning nicely.
665
666 @param level: the level we should log at (log.DEBUG, log.INFO, etc)
667 @type level: int
668 @param stackDepth: the number of stack frames to go back to get
669 file and line information, negative or zero.
670 @type stackDepth: non-positive int
671 @param name: name of the remote method
672 @type name: str
673 """
674 if level is not None:
675 debugClass = str(self.__class__).split(".")[-1].upper()
676 startArgs = [self.remoteLogName, debugClass, name]
677 format, debugArgs = flog.getFormatArgs(
678 '%s --> %s: callRemote(%s, ', startArgs,
679 ')', (), args, kwargs)
680 logKwArgs = self.doLog(level, stackDepth - 1, format,
681 *debugArgs)
682
683 if not self.mind:
684 self.warning('Tried to mindCallRemote(%s), but we are '
685 'disconnected', name)
686 return defer.fail(errors.NotConnectedError())
687
688 def callback(result):
689 format, debugArgs = flog.getFormatArgs(
690 '%s <-- %s: callRemote(%s, ', startArgs,
691 '): %r', (flog.ellipsize(result),), args, kwargs)
692 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
693 return result
694
695 def errback(failure):
696 format, debugArgs = flog.getFormatArgs(
697 '%s <-- %s: callRemote(%s, ', startArgs,
698 '): %r', (failure,), args, kwargs)
699 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
700 return failure
701
702 d = self.mind.callRemote(name, *args, **kwargs)
703 if level is not None:
704 d.addCallbacks(callback, errback)
705 return d
706
708 """
709 Call the given remote method, and log calling and returning nicely.
710
711 @param name: name of the remote method
712 @type name: str
713 """
714 return self.mindCallRemoteLogging(flog.DEBUG, -1, name, *args,
715 **kwargs)
716
718 """
719 Disconnect the remote PB client. If we are already disconnected,
720 do nothing.
721 """
722 if self.mind:
723 return self.mind.broker.transport.loseConnection()
724
726 _pingCheckInterval = configure.heartbeatInterval * 2.5
727
729 self._lastPing = time.time()
730 return defer.succeed(True)
731
736
746
748 if self._pingCheckDC:
749 self._pingCheckDC.cancel()
750 self._pingCheckDC = None
751
759 self.mind.notifyOnDisconnect(stopPingCheckingCb)
760
761
762 def _disconnect():
763 if self.mind:
764 self.mind.broker.transport.loseConnection()
765 self.startPingChecking(_disconnect)
766