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, error
31 from twisted.cred.portal import IRealm, Portal
32 from twisted.internet import protocol, defer, reactor
33 from twisted.python import log, reflect, failure
34 from twisted.spread import pb, flavors
35 from twisted.spread.pb import PBClientFactory
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.twisted import reflect as freflect
41 from flumotion.twisted import credentials as fcredentials
42 from flumotion.twisted.compat import implements
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
81 """
82 Ask the remote PB server for all the keycard interfaces it supports.
83
84 @rtype: L{defer.Deferred} returning list of str
85 """
86 def getRootObjectCb(root):
87 return root.callRemote('getKeycardClasses')
88
89 d = self.getRootObject()
90 d.addCallback(getRootObjectCb)
91 return d
92
93 - def login(self, authenticator):
120
121 def issueCb(keycard):
122 self.keycard = keycard
123 self.debug('using keycard: %r' % self.keycard)
124 return self.keycard
125
126 d = self.getKeycardClasses()
127 d.addCallback(getKeycardClassesCb)
128 d.addCallback(issueCb)
129 d.addCallback(lambda r: self.getRootObject())
130 d.addCallback(self._cbSendKeycard, authenticator, self.medium,
131 interfaces)
132 return d
133
134
135 - def _cbSendUsername(self, root, username, password, avatarId, client, interfaces):
136 self.warning("you really want to use cbSendKeycard")
137
138
139 - def _cbSendKeycard(self, root, authenticator, client, interfaces, count=0):
140 self.debug("_cbSendKeycard(root=%r, authenticator=%r, client=%r, " \
141 "interfaces=%r, count=%d" % (
142 root, authenticator, client, interfaces, count))
143 count = count + 1
144 d = root.callRemote("login", self.keycard, client, *interfaces)
145 return d.addCallback(self._cbLoginCallback, root, authenticator, client,
146 interfaces, count)
147
148
149 - def _cbLoginCallback(self, result, root, authenticator, client, interfaces,
150 count):
151 if count > 5:
152
153 self.warning('Too many recursions, internal error.')
154 self.debug("FPBClientFactory(): result %r" % result)
155
156 if not result:
157 self.warning('No result, raising.')
158 raise error.UnauthorizedLogin()
159
160 if isinstance(result, pb.RemoteReference):
161
162 self.debug('Done, returning result %r' % result)
163 return result
164
165
166 keycard = result
167 if not keycard.state == keycards.AUTHENTICATED:
168 self.debug("FPBClientFactory(): requester needs to resend %r" %
169 keycard)
170 d = authenticator.respond(keycard)
171 def _loginAgainCb(keycard):
172 d = root.callRemote("login", keycard, client, *interfaces)
173 return d.addCallback(self._cbLoginCallback, root, authenticator,
174 client, interfaces, count)
175 d.addCallback(_loginAgainCb)
176 return d
177
178 self.debug("FPBClientFactory(): authenticated %r" % keycard)
179 return keycard
180
183 """
184 Reconnecting client factory for normal PB brokers.
185
186 Users of this factory call startLogin to start logging in, and should
187 override getLoginDeferred to get the deferred returned from the PB server
188 for each login attempt.
189 """
190
192 pb.PBClientFactory.__init__(self)
193 self._doingLogin = False
194
200
202 log.msg("connection lost, reason %r" % reason)
203 pb.PBClientFactory.clientConnectionLost(self, connector, reason,
204 reconnecting=True)
205 RCF = protocol.ReconnectingClientFactory
206 RCF.clientConnectionLost(self, connector, reason)
207
215
217 self._credentials = credentials
218 self._client = client
219
220 self._doingLogin = True
221
222
224 """
225 The deferred from login is now available.
226 """
227 raise NotImplementedError
228
231 """
232 Reconnecting client factory for FPB brokers (using keycards for login).
233
234 Users of this factory call startLogin to start logging in.
235 Override getLoginDeferred to get a handle to the deferred returned
236 from the PB server.
237 """
238
243
249
256
264
265
266
268 assert not isinstance(authenticator, keycards.Keycard)
269 self._authenticator = authenticator
270 self._doingLogin = True
271
272
274 """
275 The deferred from login is now available.
276 """
277 raise NotImplementedError
278
279
280
281
282
283
284
286 """
287 Root object, used to login to bouncer.
288 """
289
290 implements(flavors.IPBRoot)
291
293 """
294 @type bouncerPortal: L{flumotion.twisted.portal.BouncerPortal}
295 """
296 self.bouncerPortal = bouncerPortal
297
300
302
303 logCategory = "_BouncerWrapper"
304
305 - def __init__(self, bouncerPortal, broker):
306 self.bouncerPortal = bouncerPortal
307 self.broker = broker
308
310 """
311 @returns: the fully-qualified class names of supported keycard
312 interfaces
313 @rtype: L{defer.Deferred} firing list of str
314 """
315 return self.bouncerPortal.getKeycardClasses()
316
318 """
319 Start of keycard login.
320
321 @param interfaces: list of fully qualified names of interface objects
322
323 @returns: one of
324 - a L{flumotion.common.keycards.Keycard} when more steps
325 need to be performed
326 - a L{twisted.spread.pb.AsReferenceable} when authentication
327 has succeeded, which will turn into a
328 L{twisted.spread.pb.RemoteReference} on the client side
329 - a L{twisted.cred.error.UnauthorizedLogin} when authentication
330 is denied
331 """
332
333 self.log("remote_login(keycard=%s, *interfaces=%r" % (keycard, interfaces))
334 interfaces = [freflect.namedAny(interface) for interface in interfaces]
335 d = self.bouncerPortal.login(keycard, mind, *interfaces)
336 d.addCallback(self._authenticateCallback, mind, *interfaces)
337 return d
338
340 self.log("_authenticateCallback(result=%r, mind=%r, interfaces=%r" % (result, mind, interfaces))
341
342
343 if not result:
344 return failure.Failure(error.UnauthorizedLogin())
345
346
347 if isinstance(result, keycards.Keycard):
348 return result
349
350
351
352
353
354 return self._loggedIn(result)
355
356 - def _loggedIn(self, (interface, perspective, logout)):
357 self.broker.notifyOnDisconnect(logout)
358 return pb.AsReferenceable(perspective, "perspective")
359
361 """
362 I am an object used by FPB clients to create keycards for me
363 and respond to challenges.
364
365 I encapsulate keycard-related data, plus secrets which are used locally
366 and not put on the keycard.
367
368 I can be serialized over PB connections to a RemoteReference and then
369 adapted with RemoteAuthenticator to present the same interface.
370
371 @cvar username: a username to log in with
372 @type username: str
373 @cvar password: a password to log in with
374 @type password: str
375 @cvar address: an address to log in from
376 @type address: str
377 @cvar avatarId: the avatarId we want to request from the PB server
378 @type avatarId: str
379 """
380 logCategory = "authenticator"
381
382 avatarId = None
383
384 username = None
385 password = None
386 address = None
387
388
390 for key in kwargs:
391 setattr(self, key, kwargs[key])
392
393 - def issue(self, keycardClasses):
432
433
437
438
441
444
446 """
447 Respond to a challenge on the given keycard, based on the secrets
448 we have.
449
450 @param keycard: the keycard with the challenge to respond to
451 @type keycard: L{keycards.Keycard}
452
453 @rtype: L{defer.Deferred} firing a {keycards.Keycard}
454 @returns: a deferred firing the keycard with a response set
455 """
456 self.debug('responding to challenge on keycard %r' % keycard)
457 methodName = "respond_%s" % keycard.__class__.__name__
458 method = getattr(self, methodName)
459 return defer.succeed(method(keycard))
460
465
470
471
474
477
479 """
480 I am an adapter for a pb.RemoteReference to present the same interface
481 as L{Authenticator}
482 """
483
484 avatarId = None
485 username = None
486 password = None
487
489 self._remote = remoteReference
490
491 - def issue(self, interfaces):
495
496 d = self._remote.callRemote('issue', interfaces)
497 d.addCallback(issueCb)
498 return d
499
502
503
505 """
506 @cvar remoteLogName: name to use to log the other side of the connection
507 @type remoteLogName: str
508 """
509 logCategory = 'referenceable'
510 remoteLogName = 'remote'
511
512
513
515 args = broker.unserialize(args)
516 kwargs = broker.unserialize(kwargs)
517 method = getattr(self, "remote_%s" % message, None)
518 if method is None:
519 raise pb.NoSuchMethod("No such method: remote_%s" % (message,))
520
521 level = flog.DEBUG
522 if message == 'ping': level = flog.LOG
523
524 debugClass = self.logCategory.upper()
525
526
527 startArgs = [self.remoteLogName, debugClass, message]
528 format, debugArgs = flog.getFormatArgs(
529 '%s --> %s: remote_%s(', startArgs,
530 ')', (), args, kwargs)
531
532 logKwArgs = self.doLog(level, method, format, *debugArgs)
533
534
535 try:
536 state = method(*args, **kwargs)
537 except TypeError:
538 self.warning("%s didn't accept %s and %s" % (method, args, kwargs))
539 raise
540
541
542 if isinstance(state, defer.Deferred):
543
544
545 def callback(result):
546 format, debugArgs = flog.getFormatArgs(
547 '%s <-- %s: remote_%s(', startArgs,
548 '): %r', (flog.ellipsize(result), ), args, kwargs)
549 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
550 return result
551 def errback(failure):
552 format, debugArgs = flog.getFormatArgs(
553 '%s <-- %s: remote_%s(', startArgs,
554 '): failure %r', (failure, ), args, kwargs)
555 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
556 return failure
557
558 state.addCallback(callback)
559 state.addErrback(errback)
560 else:
561 format, debugArgs = flog.getFormatArgs(
562 '%s <-- %s: remote_%s(', startArgs,
563 '): %r', (flog.ellipsize(state), ), args, kwargs)
564 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
565
566 return broker.serialize(state, self.perspective)
567
568 -class Avatar(pb.Avatar, flog.Loggable):
569 """
570 @cvar remoteLogName: name to use to log the other side of the connection
571 @type remoteLogName: str
572 """
573 logCategory = 'avatar'
574 remoteLogName = 'remote'
575
581
582
584 args = broker.unserialize(args)
585 kwargs = broker.unserialize(kwargs)
586 method = getattr(self, "perspective_%s" % message, None)
587 if method is None:
588 raise pb.NoSuchMethod("No such method: perspective_%s" % (message,))
589
590 level = flog.DEBUG
591 if message == 'ping': level = flog.LOG
592 debugClass = self.logCategory.upper()
593 startArgs = [self.remoteLogName, debugClass, message]
594 format, debugArgs = flog.getFormatArgs(
595 '%s --> %s: perspective_%s(', startArgs,
596 ')', (), args, kwargs)
597
598 logKwArgs = self.doLog(level, method, format, *debugArgs)
599
600
601 try:
602 state = method(*args, **kwargs)
603 except TypeError:
604 self.debug("%s didn't accept %s and %s" % (method, args, kwargs))
605 raise
606 except pb.Error, e:
607 format, debugArgs = flog.getFormatArgs(
608 '%s <-- %s: perspective_%s(', startArgs,
609 '): pb.Error %r', (e, ), args, kwargs)
610 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
611 raise e
612
613
614 if isinstance(state, defer.Deferred):
615
616
617 def callback(result):
618 format, debugArgs = flog.getFormatArgs(
619 '%s <-- %s: perspective_%s(', startArgs,
620 '): %r', (flog.ellipsize(result), ), args, kwargs)
621 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
622 return result
623 def errback(failure):
624 format, debugArgs = flog.getFormatArgs(
625 '%s <-- %s: perspective_%s(', startArgs,
626 '): failure %r', (failure, ), args, kwargs)
627 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
628 return failure
629
630 state.addCallback(callback)
631 state.addErrback(errback)
632 else:
633 format, debugArgs = flog.getFormatArgs(
634 '%s <-- %s: perspective_%s(', startArgs,
635 '): %r', (flog.ellipsize(state), ), args, kwargs)
636 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
637
638 return broker.serialize(state, self, method, args, kwargs)
639
641 """
642 Tell the avatar that the given mind has been attached.
643 This gives the avatar a way to call remotely to the client that
644 requested this avatar.
645 This is scheduled by the portal after the client has logged in.
646
647 @type mind: L{twisted.spread.pb.RemoteReference}
648 """
649 self.mind = mind
650 def nullMind(x):
651 self.debug('%r: disconnected from %r' % (self, self.mind))
652 self.mind = None
653 self.mind.notifyOnDisconnect(nullMind)
654
655 transport = self.mind.broker.transport
656 tarzan = transport.getHost()
657 jane = transport.getPeer()
658 if tarzan and jane:
659 self.debug("PB client connection seen by me is from me %s to %s" % (
660 common.addressGetHost(tarzan),
661 common.addressGetHost(jane)))
662 self.log('Client attached is mind %s', mind)
663
666 """
667 Call the given remote method, and log calling and returning nicely.
668
669 @param level: the level we should log at (log.DEBUG, log.INFO, etc)
670 @type level: int
671 @param stackDepth: the number of stack frames to go back to get
672 file and line information, negative or zero.
673 @type stackDepth: non-positive int
674 @param name: name of the remote method
675 @type name: str
676 """
677 if level is not None:
678 debugClass = str(self.__class__).split(".")[-1].upper()
679 startArgs = [self.remoteLogName, debugClass, name]
680 format, debugArgs = flog.getFormatArgs(
681 '%s --> %s: callRemote(%s, ', startArgs,
682 ')', (), args, kwargs)
683 logKwArgs = self.doLog(level, stackDepth - 1, format,
684 *debugArgs)
685
686 if not self.mind:
687 self.warning('Tried to mindCallRemote(%s), but we are '
688 'disconnected', name)
689 return defer.fail(errors.NotConnectedError())
690
691 def callback(result):
692 format, debugArgs = flog.getFormatArgs(
693 '%s <-- %s: callRemote(%s, ', startArgs,
694 '): %r', (flog.ellipsize(result), ), args, kwargs)
695 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
696 return result
697
698 def errback(failure):
699 format, debugArgs = flog.getFormatArgs(
700 '%s <-- %s: callRemote(%s', startArgs,
701 '): %r', (failure, ), args, kwargs)
702 self.doLog(level, -1, format, *debugArgs, **logKwArgs)
703 return failure
704
705 d = self.mind.callRemote(name, *args, **kwargs)
706 if level is not None:
707 d.addCallbacks(callback, errback)
708 return d
709
711 """
712 Call the given remote method, and log calling and returning nicely.
713
714 @param name: name of the remote method
715 @type name: str
716 """
717 return self.mindCallRemoteLogging(flog.DEBUG, -1, name, *args,
718 **kwargs)
719
721 """
722 Disconnect the remote PB client. If we are already disconnected,
723 do nothing.
724 """
725 if self.mind:
726 return self.mind.broker.transport.loseConnection()
727
729 _pingCheckInterval = configure.heartbeatInterval * 2.5
730
732 self._lastPing = time.time()
733 return defer.succeed(True)
734
739
749
751 if self._pingCheckDC:
752 self._pingCheckDC.cancel()
753 self._pingCheckDC = None
754
762 self.mind.notifyOnDisconnect(stopPingCheckingCb)
763
764
765 def _disconnect():
766 if self.mind:
767 self.mind.broker.transport.loseConnection()
768 self.startPingChecking(_disconnect)
769