1 <html><head><title>Evolution of Finger</title></head><body>
2 <h1>Evolution of Finger</h1>
4 <h2>Refuse Connections</h2>
7 from twisted.internet import reactor
11 <p>Here, we just run the reactor. Nothing at all will happen,
12 until we interrupt the program. It will not consume (almost)
13 no CPU resources. Not very useful, perhaps -- but this
14 is the skeleton inside which the Twisted program
20 from twisted.internet import protocol, reactor
21 class FingerProtocol(protocol.Protocol):
23 class FingerFactory(protocol.ServerFactory):
24 protocol = FingerProtocol
25 reactor.listenTCP(1079, FingerFactory())
29 <p>Here, we start listening on port 1079 [which is supposed to be
30 a reminder that eventually, we want to run on port 79, the port
31 the finger server is supposed to run on. We define a protocol which
32 does not respond to any events. Thus, connections to 1079 will
33 be accepted, but the input ignored.</p>
35 <h2>Drop Connections</h2>
38 from twisted.internet import protocol, reactor
39 class FingerProtocol(protocol.Protocol):
40 def connectionMade(self):
41 self.transport.loseConnection()
42 class FingerFactory(protocol.ServerFactory):
43 protocol = FingerProtocol
44 reactor.listenTCP(1079, FingerFactory())
48 <p>Here we add to the protocol the ability to respond to the
49 event of beginning a connection -- by terminating it.
50 Perhaps not an interesting behaviour, but it is already
51 not that far from behaving according to the letter of the
52 protocol. After all, there is no requirement to send any
53 data to the remote connection in the standard, is there.
54 The only technical problem is that we terminate the connection
55 too soon. A client which is slow enough will see his send()
56 of the username result in an error.</p>
58 <h2>Read Username, Drop Connections</h2>
61 from twisted.internet import protocol, reactor
62 from twisted.protocols import basic
63 class FingerProtocol(basic.LineReceiver):
64 def lineReceived(self, user):
65 self.transport.loseConnection()
66 class FingerFactory(protocol.ServerFactory):
67 protocol = FingerProtocol
68 reactor.listenTCP(1079, FingerFactory())
72 <p>Here we make <code>FingerProtocol</code> inherit from
73 <code>LineReceiver</code>, so that we get data-based events
74 on a line-by-line basis. We respond to the event of receiving
75 the line with shutting down the connection. Congratulations,
76 this is the first standard-compliant version of the code.
77 However, usually people actually expect some data about
78 users to be transmitted.</p>
81 <h2>Read Username, Output Error, Drop Connections</h2>
84 from twisted.internet import protocol, reactor
85 from twisted.protocols import basic
86 class FingerProtocol(basic.LineReceiver):
87 def lineReceived(self, user):
88 self.transport.write("No such user\r\n")
89 self.transport.loseConnection()
90 class FingerFactory(protocol.ServerFactory):
91 protocol = FingerProtocol
92 reactor.listenTCP(1079, FingerFactory())
96 <p>Finally, a useful version. Granted, the usefulness is somewhat
97 limited by the fact that this version only prints out a no such
98 user message. It could be used for devestating effect in honeypots,
101 <h2>Output From Empty Factory</h2>
104 # Read username, output from empty factory, drop connections
105 from twisted.internet import protocol, reactor
106 from twisted.protocols import basic
107 class FingerProtocol(basic.LineReceiver):
108 def lineReceived(self, user):
109 self.transport.write(self.factory.getUser(user)+"\r\n")
110 self.transport.loseConnection()
111 class FingerFactory(protocol.ServerFactory):
112 protocol = FingerProtocol
113 def getUser(self, user): return "No such user"
114 reactor.listenTCP(1079, FingerFactory())
118 <p>The same behaviour, but finally we see what usefuleness the
119 factory has: as something that does not get constructed for
120 every connection, it can be in charge of the user database.
121 In particular, we won't have to change the protocol if
122 the user database backend changes.</p>
124 <h2>Output from Non-empty Factory</h2>
127 # Read username, output from non-empty factory, drop connections
128 from twisted.internet import protocol, reactor
129 from twisted.protocols import basic
130 class FingerProtocol(basic.LineReceiver):
131 def lineReceived(self, user):
132 self.transport.write(self.factory.getUser(user)+"\r\n")
133 self.transport.loseConnection()
134 class FingerFactory(protocol.ServerFactory):
135 protocol = FingerProtocol
136 def __init__(self, **kwargs): self.users = kwargs
137 def getUser(self, user):
138 return self.users.get(user, "No such user")
139 reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
143 <p>Finally, a really useful finger database. While it does not
144 supply information about logged in users, it could be used to
145 distribute things like office locations and internal office
146 numbers. As hinted above, the factory is in charge of keeping
147 the user database: note that the protocol instance has not
148 changed. This is starting to look good: we really won't have
149 to keep tweaking our protocol.</p>
151 <h2>Use Deferreds</h2>
154 # Read username, output from non-empty factory, drop connections
155 # Use deferreds, to minimize synchronicity assumptions
156 from twisted.internet import protocol, reactor, defer
157 from twisted.protocols import basic
158 class FingerProtocol(basic.LineReceiver):
159 def lineReceived(self, user):
160 self.factory.getUser(user
161 ).addErrback(lambda _: "Internal error in server"
162 ).addCallback(lambda m:
163 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
164 class FingerFactory(protocol.ServerFactory):
165 protocol = FingerProtocol
166 def __init__(self, **kwargs): self.users = kwargs
167 def getUser(self, user):
168 return defer.succeed(self.users.get(user, "No such user"))
169 reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
173 <p>But, here we tweak it just for the hell of it. Yes, while the
174 previous version worked, it did assume the result of getUser is
175 always immediately available. But what if instead of an in memory
176 database, we would have to fetch result from a remote Oracle?
177 Or from the web? Or, or...</p>
179 <h2>Run 'finger' Locally</h2>
182 # Read username, output from factory interfacing to OS, drop connections
183 from twisted.internet import protocol, reactor, defer, utils
184 from twisted.protocols import basic
185 class FingerProtocol(basic.LineReceiver):
186 def lineReceived(self, user):
187 self.factory.getUser(user
188 ).addErrback(lambda _: "Internal error in server"
189 ).addCallback(lambda m:
190 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
191 class FingerFactory(protocol.ServerFactory):
192 protocol = FingerProtocol
193 def getUser(self, user):
194 return utils.getProcessOutput("finger", [user])
195 reactor.listenTCP(1079, FingerFactory())
199 <p>...from running a local command? Yes, this version (safely!) runs
200 finger locally with whatever arguments it is given, and returns the
201 standard output. This will do exactly what the standard version
202 of the finger server does -- without the need for any remote buffer
203 overflows, as the networking is done safely.</p>
205 <h2>Read Status from the Web</h2>
208 # Read username, output from factory interfacing to web, drop connections
209 from twisted.internet import protocol, reactor, defer, utils
210 from twisted.protocols import basic
211 from twisted.web import client
212 class FingerProtocol(basic.LineReceiver):
213 def lineReceived(self, user):
214 self.factory.getUser(user
215 ).addErrback(lambda _: "Internal error in server"
216 ).addCallback(lambda m:
217 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
218 class FingerFactory(protocol.ServerFactory):
219 protocol = FingerProtocol
220 def __init__(self, prefix): self.prefix=prefix
221 def getUser(self, user):
222 return client.getPage(self.prefix+user)
223 reactor.listenTCP(1079, FingerFactory(prefix='http://livejournal.com/~'))
227 <p>The web. That invention which has infiltrated homes around the
228 world finally gets through to our invention. Here we use the built-in
229 Twisted web client, which also returns a deferred. Finally, we manage
230 to have examples of three different database backends, which do
231 not change the protocol class. In fact, we will not have to change
232 the protocol again until the end of this talk: we have achieved,
233 here, one truly usable class.</p>
236 <h2>Use Application</h2>
239 # Read username, output from non-empty factory, drop connections
240 # Use deferreds, to minimize synchronicity assumptions
241 # Write application. Save in 'finger.tpy'
242 from twisted.internet import protocol, reactor, defer, app
243 from twisted.protocols import basic
244 class FingerProtocol(basic.LineReceiver):
245 def lineReceived(self, user):
246 self.factory.getUser(user
247 ).addErrback(lambda _: "Internal error in server"
248 ).addCallback(lambda m:
249 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
250 class FingerFactory(protocol.ServerFactory):
251 protocol = FingerProtocol
252 def __init__(self, **kwargs): self.users = kwargs
253 def getUser(self, user):
254 return defer.succeed(self.users.get(user, "No such user"))
255 application = app.Application('finger', uid=1, gid=1)
256 application.listenTCP(79, FingerFactory(moshez='Happy and well'))
259 <p>Up until now, we faked. We kept using port 1079, because really,
260 who wants to run a finger server with root privileges? Well, the
261 common solution is "privilege shedding": after binding to the network,
262 become a different, less privileged user. We could have done it ourselves,
263 but Twisted has a builtin way to do it. Create a snippet as above,
264 defining an application object. That object will have uid and gid
265 attributes. When running it (later we will see how) it will bind
266 to ports, shed privileges and then run.</p>
271 root% twistd -ny finger.tpy # just like before
272 root% twistd -y finger.tpy # daemonize, keep pid in twistd.pid
273 root% twistd -y finger.tpy --pidfile=finger.pid
274 root% twistd -y finger.tpy --rundir=/
275 root% twistd -y finger.tpy --chroot=/var
276 root% twistd -y finger.tpy -l /var/log/finger.log
277 root% twistd -y finger.tpy --syslog # just log to syslog
278 root% twistd -y finger.tpy --syslog --prefix=twistedfinger # use given prefix
281 <p>This is how to run "Twisted Applications" -- files which define an
282 'application'. twistd (TWISTed Daemonizer) does everything a daemon
283 can be expected to -- shuts down stdin/stdout/stderr, disconnects
284 from the terminal and can even change runtime directory, or even
285 the root filesystems. In short, it does everything so the Twisted
286 application developer can concentrate on writing his networking code.</p>
288 <h2>Setting Message By Local Users</h2>
291 # But let's try and fix setting away messages, shall we?
292 from twisted.internet import protocol, reactor, defer, app
293 from twisted.protocols import basic
294 class FingerProtocol(basic.LineReceiver):
295 def lineReceived(self, user):
296 self.factory.getUser(user
297 ).addErrback(lambda _: "Internal error in server"
298 ).addCallback(lambda m:
299 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
300 class FingerFactory(protocol.ServerFactory):
301 protocol = FingerProtocol
302 def __init__(self, **kwargs): self.users = kwargs
303 def getUser(self, user):
304 return defer.succeed(self.users.get(user, "No such user"))
305 class FingerSetterProtocol(basic.LineReceiver):
306 def connectionMade(self): self.lines = []
307 def lineReceived(self, line): self.lines.append(line)
308 def connectionLost(self): self.factory.setUser(*self.lines)
309 class FingerSetterFactory(protocol.ServerFactory):
310 def __init__(self, ff): self.setUser = self.ff.users.__setitem__
311 ff = FingerFactory(moshez='Happy and well')
312 fsf = FingerSetterFactory(ff)
313 application = app.Application('finger', uid=1, gid=1)
314 application.listenTCP(79, ff)
315 application.listenTCP(1079, fsf, interface='127.0.0.1')
318 <p>Now that port 1079 is free, maybe we can run on it a different
319 server, one which will let people set their messages. It does
320 no access control, so anyone who can login to the machine can
321 set any message. We assume this is the desired behaviour in
322 our case. Testing it can be done by simply:
328 Giving a talk now, sorry!
332 <h2>Use Services to Make Dependencies Sane</h2>
336 from twisted.internet import protocol, reactor, defer, app
337 from twisted.protocols import basic
338 class FingerProtocol(basic.LineReceiver):
339 def lineReceived(self, user):
340 self.factory.getUser(user
341 ).addErrback(lambda _: "Internal error in server"
342 ).addCallback(lambda m:
343 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
344 class FingerSetterProtocol(basic.LineReceiver):
345 def connectionMade(self): self.lines = []
346 def lineReceived(self, line): self.lines.append(line)
347 def connectionLost(self): self.factory.setUser(*self.lines)
348 class FingerService(app.ApplicationService):
349 def __init__(self, *args, **kwargs):
350 app.ApplicationService.__init__(self, *args)
352 def getUser(self, user):
353 return defer.succeed(self.users.get(u, "No such user"))
354 def getFingerFactory(self):
355 f = protocol.ServerFactory()
356 f.protocol, f.getUser = FingerProtocol, self.getUser
358 def getFingerSetterFactory(self):
359 f = protocol.ServerFactory()
360 f.protocol, f.setUser = FingerSetterProtocol, self.users.__setitem__
362 application = app.Application('finger', uid=1, gid=1)
363 f = FingerService(application, 'finger', moshez='Happy and well')
364 application.listenTCP(79, f.getFingerFactory())
365 application.listenTCP(1079, f.getFingerSetterFactory(), interface='127.0.0.1')
368 <p>The previous version had the setter poke at the innards of the
369 finger factory. It's usually not a good idea: this version makes
370 both factories symmetric by making them both look at a single
371 object. Services are useful for when an object is needed which is
372 not related to a specific network server. Here, we moved all responsibility
373 for manufacturing factories into the service. Note that we stopped
374 subclassing: the service simply puts useful methods and attributes
375 inside the factories. We are getting better at protocol design:
376 none of our protocol classes had to be changed, and neither will
377 have to change until the end of the talk.</p>
379 <h2>Read Status File</h2>
383 from twisted.internet import protocol, reactor, defer, app
384 from twisted.protocols import basic
385 class FingerProtocol(basic.LineReceiver):
386 def lineReceived(self, user):
387 self.factory.getUser(user
388 ).addErrback(lambda _: "Internal error in server"
389 ).addCallback(lambda m:
390 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
391 class FingerSetterProtocol(basic.LineReceiver):
392 def connectionMade(self): self.lines = []
393 def lineReceived(self, line): self.lines.append(line)
394 def connectionLost(self): self.factory.setUser(*self.lines)
395 class FingerService(app.ApplicationService):
396 def __init__(self, file, *args, **kwargs):
397 app.ApplicationService.__init__(self, *args, **kwargs)
399 def startService(self):
400 app.ApplicationService.startService(self)
404 for line in file(self.file):
405 user, status = line.split(':', 1)
406 self.users[user] = status
407 self.call = reactor.callLater(30, self._read)
408 def stopService(self):
409 app.ApplicationService.stopService(self)
411 def getUser(self, user):
412 return defer.succeed(self.users.get(u, "No such user"))
413 def getFingerFactory(self):
414 f = protocol.ServerFactory()
415 f.protocol, f.getUser = FingerProtocol, self.getUser
417 application = app.Application('finger', uid=1, gid=1)
418 f = FingerService('/etc/users', application, 'finger')
419 application.listenTCP(79, f.getFingerFactory())
422 <p>This version shows how, instead of just letting users set their
423 messages, we can read those from a centrally managed file. We cache
424 results, and every 30 seconds we refresh it. Services are useful
425 for such scheduled tasks.</p>
427 <h2>Announce on Web, Too</h2>
430 # Read from file, announce on the web!
431 from twisted.internet import protocol, reactor, defer, app
432 from twisted.protocols import basic
433 from twisted.web import resource, server, static
435 class FingerProtocol(basic.LineReceiver):
436 def lineReceived(self, user):
437 self.factory.getUser(user
438 ).addErrback(lambda _: "Internal error in server"
439 ).addCallback(lambda m:
440 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
441 class FingerSetterProtocol(basic.LineReceiver):
442 def connectionMade(self): self.lines = []
443 def lineReceived(self, line): self.lines.append(line)
444 def connectionLost(self): self.factory.setUser(*self.lines)
445 class FingerService(app.ApplicationService):
446 def __init__(self, file, *args, **kwargs):
447 app.ApplicationService.__init__(self, *args, **kwargs)
449 def startService(self):
450 app.ApplicationService.startService(self)
454 for line in file(self.file):
455 user, status = line.split(':', 1)
456 self.users[user] = status
457 self.call = reactor.callLater(30, self._read)
458 def stopService(self):
459 app.ApplicationService.stopService(self)
461 def getUser(self, user):
462 return defer.succeed(self.users.get(u, "No such user"))
463 def getFingerFactory(self):
464 f = protocol.ServerFactory()
465 f.protocol, f.getUser = FingerProtocol, self.getUser
467 def getResource(self):
468 r = resource.Resource()
469 r.getChild = (lambda path, request:
470 static.Data('text/html',
471 '<h1>%s</h1><p>%s</p>' %
472 tuple(map(cgi.escape,
473 [path,self.users.get(path, "No such user")]))))
474 application = app.Application('finger', uid=1, gid=1)
475 f = FingerService('/etc/users', application, 'finger')
476 application.listenTCP(79, f.getFingerFactory())
477 application.listenTCP(80, server.Site(f.getResource()))
480 <p>The same kind of service can also produce things useful for
481 other protocols. For example, in twisted.web, the factory
482 itself (the site) is almost never subclassed -- instead,
483 it is given a resource, which represents the tree of resources
484 available via URLs. That hierarchy is navigated by site,
485 and overriding it dynamically is possible with getChild.</p>
487 <h2>Announce on IRC, Too</h2>
490 # Read from file, announce on the web, irc
491 from twisted.internet import protocol, reactor, defer, app
492 from twisted.protocols import basic, irc
493 from twisted.web import resource, server, static
495 class FingerProtocol(basic.LineReceiver):
496 def lineReceived(self, user):
497 self.factory.getUser(user
498 ).addErrback(lambda _: "Internal error in server"
499 ).addCallback(lambda m:
500 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
501 class FingerSetterProtocol(basic.LineReceiver):
502 def connectionMade(self): self.lines = []
503 def lineReceived(self, line): self.lines.append(line)
504 def connectionLost(self): self.factory.setUser(*self.lines)
505 class IRCReplyBot(irc.IRCClient):
506 def connectionMade(self):
507 self.nickname = self.factory.nickname
508 irc.IRCClient.connectionMade(self)
509 def privmsg(self, user, channel, msg):
510 if user.lower() == channel.lower():
511 self.factory.getUser(msg
512 ).addErrback(lambda _: "Internal error in server"
513 ).addCallback(lambda m: self.msg(user, m))
514 class FingerService(app.ApplicationService):
515 def __init__(self, file, *args, **kwargs):
516 app.ApplicationService.__init__(self, *args, **kwargs)
518 def startService(self):
519 app.ApplicationService.startService(self)
523 for line in file(self.file):
524 user, status = line.split(':', 1)
525 self.users[user] = status
526 self.call = reactor.callLater(30, self._read)
527 def stopService(self):
528 app.ApplicationService.stopService(self)
530 def getUser(self, user):
531 return defer.succeed(self.users.get(u, "No such user"))
532 def getFingerFactory(self):
533 f = protocol.ServerFactory()
534 f.protocol, f.getUser = FingerProtocol, self.getUser
536 def getResource(self):
537 r = resource.Resource()
538 r.getChild = (lambda path, request:
539 static.Data('text/html',
540 '<h1>%s</h1><p>%s</p>' %
541 tuple(map(cgi.escape,
542 [path,self.users.get(path, "No such user")]))))
543 def getIRCBot(self, nickname):
544 f = protocol.ReconnectingClientFactory()
545 f.protocol,f.nickname,f.getUser = IRCReplyBot,nickname,self.getUser
547 application = app.Application('finger', uid=1, gid=1)
548 f = FingerService('/etc/users', application, 'finger')
549 application.listenTCP(79, f.getFingerFactory())
550 application.listenTCP(80, server.Site(f.getResource()))
551 application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
554 <p>This is the first time there is client code. IRC clients often
555 act a lot like servers: responding to events form the network.
556 The reconnecting client factory will make sure that severed links
557 will get re-established, with intelligent tweaked exponential
558 backoff algorithms. The irc client itself is simple: the only
559 real hack is getting the nickname from the factory in connectionMade.</p>
563 <h2>Add XML-RPC Support</h2>
566 # Read from file, announce on the web, irc, xml-rpc
567 from twisted.internet import protocol, reactor, defer, app
568 from twisted.protocols import basic, irc
569 from twisted.web import resource, server, static, xmlrpc
571 class FingerProtocol(basic.LineReceiver):
572 def lineReceived(self, user):
573 self.factory.getUser(user
574 ).addErrback(lambda _: "Internal error in server"
575 ).addCallback(lambda m:
576 (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
577 class FingerSetterProtocol(basic.LineReceiver):
578 def connectionMade(self): self.lines = []
579 def lineReceived(self, line): self.lines.append(line)
580 def connectionLost(self): self.factory.setUser(*self.lines)
581 class IRCReplyBot(irc.IRCClient):
582 def connectionMade(self):
583 self.nickname = self.factory.nickname
584 irc.IRCClient.connectionMade(self)
585 def privmsg(self, user, channel, msg):
586 if user.lower() == channel.lower():
587 self.factory.getUser(msg
588 ).addErrback(lambda _: "Internal error in server"
589 ).addCallback(lambda m: self.msg(user, m))
590 class FingerService(app.ApplicationService):
591 def __init__(self, file, *args, **kwargs):
592 app.ApplicationService.__init__(self, *args, **kwargs)
594 def startService(self):
595 app.ApplicationService.startService(self)
599 for line in file(self.file):
600 user, status = line.split(':', 1)
601 self.users[user] = status
602 self.call = reactor.callLater(30, self._read)
603 def stopService(self):
604 app.ApplicationService.stopService(self)
606 def getUser(self, user):
607 return defer.succeed(self.users.get(u, "No such user"))
608 def getFingerFactory(self):
609 f = protocol.ServerFactory()
610 f.protocol, f.getUser = FingerProtocol, self.getUser
612 def getResource(self):
613 r = resource.Resource()
614 r.getChild = (lambda path, request:
615 static.Data('text/html',
616 '<h1>%s</h1><p>%s</p>' %
617 tuple(map(cgi.escape,
618 [path,self.users.get(path, "No such user")]))))
620 x.xmlrpc_getUser = self.getUser
621 r.putChild('RPC2.0', x)
623 def getIRCBot(self, nickname):
624 f = protocol.ReconnectingClientFactory()
625 f.protocol,f.nickname,f.getUser = IRCReplyBot,nickname,self.getUser
627 application = app.Application('finger', uid=1, gid=1)
628 f = FingerService('/etc/users', application, 'finger')
629 application.listenTCP(79, f.getFingerFactory())
630 application.listenTCP(80, server.Site(f.getResource()))
631 application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
634 <p>In Twisted, XML-RPC support is handled just as though it was
635 another resource. That resource will still support GET calls normally
636 through render(), but that is usually left unimplemented. Note
637 that it is possible to return deferreds from XML-RPC methods.
638 The client, of course, will not get the answer until the deferred
642 <h2>Write Readable Code</h2>
645 # Do everything properly
646 from twisted.internet import protocol, reactor, defer, app
647 from twisted.protocols import basic, irc
648 from twisted.web import resource, server, static, xmlrpc
652 return "Internal error in server"
654 class FingerProtocol(basic.LineReceiver):
656 def lineReceived(self, user):
657 d = self.factory.getUser(user)
658 d.addErrback(catchError)
659 def writeValue(value):
660 self.transport.write(value)
661 self.transport.loseConnection()
662 d.addCallback(writeValue)
665 class FingerSetterProtocol(basic.LineReceiver):
667 def connectionMade(self):
670 def lineReceived(self, line):
671 self.lines.append(line)
673 def connectionLost(self):
674 if len(self.lines) == 2:
675 self.factory.setUser(*self.lines)
678 class IRCReplyBot(irc.IRCClient):
680 def connectionMade(self):
681 self.nickname = self.factory.nickname
682 irc.IRCClient.connectionMade(self)
684 def privmsg(self, user, channel, msg):
685 if user.lower() == channel.lower():
686 d = self.factory.getUser(msg)
687 d.addErrback(catchError)
688 d.addCallback(lambda m: "Status of %s: %s" % (user, m))
689 d.addCallback(lambda m: self.msg(user, m))
692 class UserStatusTree(resource.Resource):
694 def __init__(self, service):
695 resource.Resource.__init__(self):
696 self.service = service
698 def render(self, request):
699 d = self.service.getUsers()
700 def formatUsers(users):
701 l = ["<li><a href="%s">%s</a></li> % (user, user)
703 return '<ul>'+''.join(l)+'</ul>'
704 d.addCallback(formatUsers)
705 d.addCallback(request.write)
706 d.addCallback(lambda _: request.finish())
707 return server.NOT_DONE_YET
709 def getChild(self, path, request):
710 return UserStatus(path, self.service)
713 class UserStatus(resource.Resource):
715 def __init__(self, user, service):
716 resource.Resource.__init__(self):
718 self.service = service
720 def render(self, request):
721 d = self.service.getUser(self.user)
722 d.addCallback(cgi.escape)
723 d.addCallback(lambda m:
724 '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
725 d.addCallback(request.write)
726 d.addCallback(lambda _: request.finish())
727 return server.NOT_DONE_YET
730 class UserStatusXR(xmlrpc.XMLPRC):
732 def __init__(self, service):
733 xmlrpc.XMLRPC.__init__(self)
734 self.service = service
736 def xmlrpc_getUser(self, user):
737 return self.service.getUser(user)
740 class FingerService(app.ApplicationService):
742 def __init__(self, file, *args, **kwargs):
743 app.ApplicationService.__init__(self, *args, **kwargs)
746 def startService(self):
747 app.ApplicationService.startService(self)
752 for line in file(self.file):
753 user, status = line.split(':', 1)
754 self.users[user] = status
755 self.call = reactor.callLater(30, self._read)
757 def stopService(self):
758 app.ApplicationService.stopService(self)
761 def getUser(self, user):
762 return defer.succeed(self.users.get(u, "No such user"))
765 return defer.succeed(self.users.keys())
767 def getFingerFactory(self):
768 f = protocol.ServerFactory()
769 f.protocol = FingerProtocol
770 f.getUser = self.getUser
773 def getResource(self):
774 r = UserStatusTree(self)
775 x = UserStatusXR(self)
776 r.putChild('RPC2.0', x)
779 def getIRCBot(self, nickname):
780 f = protocol.ReconnectingClientFactory()
781 f.protocol = IRCReplyBot
782 f.nickname = nickname
783 f.getUser = self.getUser
786 application = app.Application('finger', uid=1, gid=1)
787 f = FingerService('/etc/users', application, 'finger')
788 application.listenTCP(79, f.getFingerFactory())
789 application.listenTCP(80, server.Site(f.getResource()))
790 application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
793 <p>The last version of the application had a lot of hacks. We avoided
794 subclassing, did not support things like user listings in the web
795 support, and removed all blank lines -- all in the interest of code
796 which is shorter. Here we take a step back, subclass what is more
797 naturally a subclass, make things which should take multiple lines
798 take them, etc. This shows a much better style of developing Twisted
799 applications, though the hacks in the previous stages are sometimes
800 used in throw-away prototypes.</p>
802 <h2>Write Maintainable Code</h2>
805 # Do everything properly, and componentize
806 from twisted.internet import protocol, reactor, defer, app
807 from twisted.protocols import basic, irc
808 from twisted.python import components
809 from twisted.web import resource, server, static, xmlrpc
812 class IFingerService(components.Interface):
814 def getUser(self, user):
815 '''Return a deferred returning a string'''
818 '''Return a deferred returning a list of strings'''
820 class IFingerSettingService(components.Interface):
822 def setUser(self, user, status):
823 '''Set the user's status to something'''
826 return "Internal error in server"
829 class FingerProtocol(basic.LineReceiver):
831 def lineReceived(self, user):
832 d = self.factory.getUser(user)
833 d.addErrback(catchError)
834 def writeValue(value):
835 self.transport.write(value)
836 self.transport.loseConnection()
837 d.addCallback(writeValue)
840 class IFingerFactory(components.Interface):
842 def getUser(self, user):
843 """Return a deferred returning a string""""
845 def buildProtocol(self, addr):
846 """Return a protocol returning a string""""
849 class FingerFactoryFromService(protocol.ServerFactory):
851 __implements__ = IFingerFactory,
853 protocol = FingerProtocol
855 def __init__(self, service):
856 self.service = service
858 def getUser(self, user):
859 return self.service.getUser(user)
861 components.registerAdapter(FingerFactoryFromService, IFingerService)
864 class FingerSetterProtocol(basic.LineReceiver):
866 def connectionMade(self):
869 def lineReceived(self, line):
870 self.lines.append(line)
872 def connectionLost(self):
873 if len(self.lines) == 2:
874 self.factory.setUser(*self.lines)
877 class IFingerSetterFactory(components.Interface):
879 def setUser(self, user, status):
880 """Return a deferred returning a string"""
882 def buildProtocol(self, addr):
883 """Return a protocol returning a string"""
886 class FingerSetterFactoryFromService(protocol.ServerFactory):
888 __implements__ = IFingerSetterFactory,
890 protocol = FingerSetterProtocol
892 def __init__(self, service):
893 self.service = service
895 def setUser(self, user, status):
896 self.service.setUser(user, status)
899 components.registerAdapter(FingerSetterFactoryFromService,
900 IFingerSettingService)
902 class IRCReplyBot(irc.IRCClient):
904 def connectionMade(self):
905 self.nickname = self.factory.nickname
906 irc.IRCClient.connectionMade(self)
908 def privmsg(self, user, channel, msg):
909 if user.lower() == channel.lower():
910 d = self.factory.getUser(msg)
911 d.addErrback(catchError)
912 d.addCallback(lambda m: "Status of %s: %s" % (user, m))
913 d.addCallback(lambda m: self.msg(user, m))
916 class IIRCClientFactory(components.Interface):
922 def getUser(self, user):
923 """Return a deferred returning a string"""
925 def buildProtocol(self, addr):
926 """Return a protocol"""
929 class IRCClientFactoryFromService(protocol.ClientFactory):
931 __implements__ = IIRCClientFactory,
933 protocol = IRCReplyBot
936 def __init__(self, service):
937 self.service = service
939 def getUser(self, user):
940 return self.service.getUser()
942 components.registerAdapter(IRCClientFactoryFromService, IFingerService)
944 class UserStatusTree(resource.Resource):
946 def __init__(self, service):
947 resource.Resource.__init__(self):
948 self.putChild('RPC2.0', UserStatusXR(self.service))
949 self.service = service
951 def render(self, request):
952 d = self.service.getUsers()
953 def formatUsers(users):
954 l = ["<li><a href="%s">%s</a></li> % (user, user)
956 return '<ul>'+''.join(l)+'</ul>'
957 d.addCallback(formatUsers)
958 d.addCallback(request.write)
959 d.addCallback(lambda _: request.finish())
960 return server.NOT_DONE_YET
962 def getChild(self, path, request):
963 return UserStatus(path, self.service)
965 components.registerAdapter(UserStatusTree, IFingerService)
967 class UserStatus(resource.Resource):
969 def __init__(self, user, service):
970 resource.Resource.__init__(self):
972 self.service = service
974 def render(self, request):
975 d = self.service.getUser(self.user)
976 d.addCallback(cgi.escape)
977 d.addCallback(lambda m:
978 '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
979 d.addCallback(request.write)
980 d.addCallback(lambda _: request.finish())
981 return server.NOT_DONE_YET
984 class UserStatusXR(xmlrpc.XMLPRC):
986 def __init__(self, service):
987 xmlrpc.XMLRPC.__init__(self)
988 self.service = service
990 def xmlrpc_getUser(self, user):
991 return self.service.getUser(user)
994 class FingerService(app.ApplicationService):
996 __implements__ = IFingerService,
998 def __init__(self, file, *args, **kwargs):
999 app.ApplicationService.__init__(self, *args, **kwargs)
1002 def startService(self):
1003 app.ApplicationService.startService(self)
1008 for line in file(self.file):
1009 user, status = line.split(':', 1)
1010 self.users[user] = status
1011 self.call = reactor.callLater(30, self._read)
1013 def stopService(self):
1014 app.ApplicationService.stopService(self)
1017 def getUser(self, user):
1018 return defer.succeed(self.users.get(u, "No such user"))
1021 return defer.succeed(self.users.keys())
1024 application = app.Application('finger', uid=1, gid=1)
1025 f = FingerService('/etc/users', application, 'finger')
1026 application.listenTCP(79, IFingerFactory(f))
1027 application.listenTCP(80, server.Site(resource.IResource(f)))
1028 i = IIRCClientFactory(f)
1029 i.nickname = 'fingerbot'
1030 application.connectTCP('irc.freenode.org', 6667, i)
1033 <p>In the last version, the service class was three times longer than
1034 any other class, and was hard to understand. This was because it turned
1035 out to have multiple responsibilities. It had to know how to access
1036 user information, by scheduling a reread of the file ever half minute,
1037 but also how to display itself in a myriad of protocols. Here, we
1038 used the component-based architecture that Twisted provides to achieve
1039 a separation of concerns. All the service is responsible for, now,
1040 is supporting getUser/getUsers. It declares its support via the
1041 __implements__ keyword. Then, adapters are used to make this service
1042 look like an appropriate class for various things: for supplying
1043 a finger factory to listenTCP, for supplying a resource to site's
1044 constructor, and to provide an IRC client factory for connectTCP.
1045 All the adapters use are the methods in FingerService they are
1046 declared to use: getUser/getUsers. We could, of course,
1047 skipped the interfaces and let the configuration code use
1048 things like FingerFactoryFromService(f) directly. However, using
1049 interfaces provides the same flexibility inheritance gives: future
1050 subclasses can override the adapters.</p>
1054 <h2>Advantages of Latest Version</h2>
1057 <li>Readable -- each class is short</li>
1058 <li>Maintainable -- each class knows only about interfaces</li>
1059 <li>Dependencies between code parts are minimized</li>
1060 <li>Example: writing a new IFingerService is easy</li>
1064 class MemoryFingerService(app.ApplicationService):
1065 __implements__ = IFingerService, IFingerSetterService
1067 def __init__(self, *args, **kwargs):
1068 app.ApplicationService.__init__(self, *args)
1071 def getUser(self, user):
1072 return defer.succeed(self.users.get(u, "No such user"))
1075 return defer.succeed(self.users.keys())
1077 def setUser(self, user, status):
1078 self.users[user] = status
1080 application = app.Application('finger', uid=1, gid=1)
1081 # New constructor call
1082 f = MemoryFingerService(application, 'finger', moshez='Happy and well')
1083 application.listenTCP(79, IFingerFactory(f))
1084 application.listenTCP(80, server.Site(resource.IResource(f)))
1085 i = IIRCClientFactory(f)
1086 i.nickname = 'fingerbot'
1087 application.connectTCP('irc.freenode.org', 6667, i)
1088 # New: run setter too
1089 application.listenTCP(1079, IFingerSetterFactory(f), interface='127.0.0.1')
1092 <p>Here we show just how convenient it is to implement new backends
1093 when we move to a component based architecture. Note that here
1094 we also use an interface we previously wrote, FingerSetterFactory,
1095 by supporting one single method. We manage to preserve the service's
1096 ignorance of the network.</p>
1098 <h2>Another Backend</h2>
1101 class LocalFingerService(app.ApplicationService):
1102 __implements__ = IFingerService
1104 def getUser(self, user):
1105 return utils.getProcessOutput("finger", [user])
1108 return defer.succeed([])
1110 application = app.Application('finger', uid=1, gid=1)
1111 f = LocalFingerService(application, 'finger')
1112 application.listenTCP(79, IFingerFactory(f))
1113 application.listenTCP(80, server.Site(resource.IResource(f)))
1114 i = IIRCClientFactory(f)
1115 i.nickname = 'fingerbot'
1116 application.connectTCP('irc.freenode.org', 6667, i)
1119 <p>We have already wrote this, but now we get more for less work:
1120 the network code is completely separate from the backend.</p>
1122 <h2>Yet Another Backend: Doing the Standard Thing</h2>
1127 class LocalFingerService(app.ApplicationService):
1128 __implements__ = IFingerService
1130 def getUser(self, user):
1132 entry = pwd.getpwnam(user)
1134 return "No such user"
1136 f=file(os.path.join(entry[5],'.plan'))
1137 except (IOError, OSError):
1138 return "No such user"
1144 return defer.succeed([])
1146 application = app.Application('finger', uid=1, gid=1)
1147 f = LocalFingerService(application, 'finger')
1148 application.listenTCP(79, IFingerFactory(f))
1149 application.listenTCP(80, server.Site(resource.IResource(f)))
1150 i = IIRCClientFactory(f)
1151 i.nickname = 'fingerbot'
1152 application.connectTCP('irc.freenode.org', 6667, i)
1155 <p>Not much to say about that, except to indicate that by now we
1156 can be churning out backends like crazy. Feel like doing a backend
1157 for advogato, for example? Dig out the XML-RPC client support Twisted
1158 has, and get to work!</p>
1161 <h2>Aspect Oriented Programming</h2>
1164 <li>This is an example...</li>
1165 <li>...with something actually useful...</li>
1166 <li>...not logging and timing.</li>
1167 <li>Write less code, have less dependencies!</li>
1173 # Do everything properly, and componentize
1174 from twisted.internet import protocol, reactor, defer, app
1175 from twisted.protocols import basic, irc
1176 from twisted.python import components
1177 from twisted.web import resource, server, static, xmlrpc, microdom
1178 from twisted.web.woven import page, widget
1181 class IFingerService(components.Interface):
1183 def getUser(self, user):
1184 '''Return a deferred returning a string'''
1187 '''Return a deferred returning a list of strings'''
1189 class IFingerSettingService(components.Interface):
1191 def setUser(self, user, status):
1192 '''Set the user's status to something'''
1194 def catchError(err):
1195 return "Internal error in server"
1198 class FingerProtocol(basic.LineReceiver):
1200 def lineReceived(self, user):
1201 d = self.factory.getUser(user)
1202 d.addErrback(catchError)
1203 def writeValue(value):
1204 self.transport.write(value)
1205 self.transport.loseConnection()
1206 d.addCallback(writeValue)
1209 class IFingerFactory(components.Interface):
1211 def getUser(self, user):
1212 """Return a deferred returning a string""""
1214 def buildProtocol(self, addr):
1215 """Return a protocol returning a string""""
1218 class FingerFactoryFromService(protocol.ServerFactory):
1220 __implements__ = IFingerFactory,
1222 protocol = FingerProtocol
1224 def __init__(self, service):
1225 self.service = service
1227 def getUser(self, user):
1228 return self.service.getUser(user)
1230 components.registerAdapter(FingerFactoryFromService, IFingerService)
1233 class FingerSetterProtocol(basic.LineReceiver):
1235 def connectionMade(self):
1238 def lineReceived(self, line):
1239 self.lines.append(line)
1241 def connectionLost(self):
1242 if len(self.lines) == 2:
1243 self.factory.setUser(*self.lines)
1246 class IFingerSetterFactory(components.Interface):
1248 def setUser(self, user, status):
1249 """Return a deferred returning a string"""
1251 def buildProtocol(self, addr):
1252 """Return a protocol returning a string"""
1255 class FingerSetterFactoryFromService(protocol.ServerFactory):
1257 __implements__ = IFingerSetterFactory,
1259 protocol = FingerSetterProtocol
1261 def __init__(self, service):
1262 self.service = service
1264 def setUser(self, user, status):
1265 self.service.setUser(user, status)
1268 components.registerAdapter(FingerSetterFactoryFromService,
1269 IFingerSettingService)
1271 class IRCReplyBot(irc.IRCClient):
1273 def connectionMade(self):
1274 self.nickname = self.factory.nickname
1275 irc.IRCClient.connectionMade(self)
1277 def privmsg(self, user, channel, msg):
1278 if user.lower() == channel.lower():
1279 d = self.factory.getUser(msg)
1280 d.addErrback(catchError)
1281 d.addCallback(lambda m: "Status of %s: %s" % (user, m))
1282 d.addCallback(lambda m: self.msg(user, m))
1285 class IIRCClientFactory(components.Interface):
1291 def getUser(self, user):
1292 """Return a deferred returning a string"""
1294 def buildProtocol(self, addr):
1295 """Return a protocol"""
1298 class IRCClientFactoryFromService(protocol.ClientFactory):
1300 __implements__ = IIRCClientFactory,
1302 protocol = IRCReplyBot
1305 def __init__(self, service):
1306 self.service = service
1308 def getUser(self, user):
1309 return self.service.getUser()
1311 components.registerAdapter(IRCClientFactoryFromService, IFingerService)
1314 class UsersModel(model.MethodModel):
1316 def __init__(self, service):
1317 self.service = service
1319 def wmfactory_users(self):
1320 return self.service.getUsers()
1322 components.registerAdapter(UsersModel, IFingerService)
1324 class UserStatusTree(page.Page):
1326 template = """<html><head><title>Users</title><head><body>
1327 <h1>Users</h1>
1328 <ul model="users" view="List">
1329 <li pattern="listItem" /><a view="Link" model="."
1330 href="dummy"><span model="." view="Text" /></a>
1331 </ul></body></html>"""
1333 def initialize(self, **kwargs):
1334 self.putChild('RPC2.0', UserStatusXR(self.model.service))
1336 def getDynamicChild(self, path, request):
1337 return UserStatus(user=path, service=self.model.service)
1339 components.registerAdapter(UserStatusTree, IFingerService)
1342 class UserStatus(page.Page):
1344 template='''<html><head><title view="Text" model="user"/></heaD>
1345 <body><h1 view="Text" model="user"/>
1346 <p mode="status" view="Text" />
1347 </body></html>'''
1349 def initialize(self, **kwargs):
1350 self.user = kwargs['user']
1351 self.service = kwargs['service']
1353 def wmfactory_user(self):
1356 def wmfactory_status(self):
1357 return self.service.getUser(self.user)
1359 class UserStatusXR(xmlrpc.XMLPRC):
1361 def __init__(self, service):
1362 xmlrpc.XMLRPC.__init__(self)
1363 self.service = service
1365 def xmlrpc_getUser(self, user):
1366 return self.service.getUser(user)
1369 class FingerService(app.ApplicationService):
1371 __implements__ = IFingerService,
1373 def __init__(self, file, *args, **kwargs):
1374 app.ApplicationService.__init__(self, *args, **kwargs)
1377 def startService(self):
1378 app.ApplicationService.startService(self)
1383 for line in file(self.file):
1384 user, status = line.split(':', 1)
1385 self.users[user] = status
1386 self.call = reactor.callLater(30, self._read)
1388 def stopService(self):
1389 app.ApplicationService.stopService(self)
1392 def getUser(self, user):
1393 return defer.succeed(self.users.get(u, "No such user"))
1396 return defer.succeed(self.users.keys())
1399 application = app.Application('finger', uid=1, gid=1)
1400 f = FingerService('/etc/users', application, 'finger')
1401 application.listenTCP(79, IFingerFactory(f))
1402 application.listenTCP(80, server.Site(resource.IResource(f)))
1403 i = IIRCClientFactory(f)
1404 i.nickname = 'fingerbot'
1405 application.connectTCP('irc.freenode.org', 6667, i)
1408 <p>Here we convert to using Woven, instead of manually
1409 constructing HTML snippets. Woven is a sophisticated web templating
1410 system. Its main features are to disallow any code inside the HTML,
1411 and transparent integration with deferred results.</p>
1413 <h2>Use Perspective Broker</h2>
1416 # Do everything properly, and componentize
1417 from twisted.internet import protocol, reactor, defer, app
1418 from twisted.protocols import basic, irc
1419 from twisted.python import components
1420 from twisted.web import resource, server, static, xmlrpc, microdom
1421 from twisted.web.woven import page, widget
1422 from twisted.spread import pb
1425 class IFingerService(components.Interface):
1427 def getUser(self, user):
1428 '''Return a deferred returning a string'''
1431 '''Return a deferred returning a list of strings'''
1433 class IFingerSettingService(components.Interface):
1435 def setUser(self, user, status):
1436 '''Set the user's status to something'''
1438 def catchError(err):
1439 return "Internal error in server"
1442 class FingerProtocol(basic.LineReceiver):
1444 def lineReceived(self, user):
1445 d = self.factory.getUser(user)
1446 d.addErrback(catchError)
1447 def writeValue(value):
1448 self.transport.write(value)
1449 self.transport.loseConnection()
1450 d.addCallback(writeValue)
1453 class IFingerFactory(components.Interface):
1455 def getUser(self, user):
1456 """Return a deferred returning a string""""
1458 def buildProtocol(self, addr):
1459 """Return a protocol returning a string""""
1462 class FingerFactoryFromService(protocol.ServerFactory):
1464 __implements__ = IFingerFactory,
1466 protocol = FingerProtocol
1468 def __init__(self, service):
1469 self.service = service
1471 def getUser(self, user):
1472 return self.service.getUser(user)
1474 components.registerAdapter(FingerFactoryFromService, IFingerService)
1477 class FingerSetterProtocol(basic.LineReceiver):
1479 def connectionMade(self):
1482 def lineReceived(self, line):
1483 self.lines.append(line)
1485 def connectionLost(self):
1486 if len(self.lines) == 2:
1487 self.factory.setUser(*self.lines)
1490 class IFingerSetterFactory(components.Interface):
1492 def setUser(self, user, status):
1493 """Return a deferred returning a string"""
1495 def buildProtocol(self, addr):
1496 """Return a protocol returning a string"""
1499 class FingerSetterFactoryFromService(protocol.ServerFactory):
1501 __implements__ = IFingerSetterFactory,
1503 protocol = FingerSetterProtocol
1505 def __init__(self, service):
1506 self.service = service
1508 def setUser(self, user, status):
1509 self.service.setUser(user, status)
1512 components.registerAdapter(FingerSetterFactoryFromService,
1513 IFingerSettingService)
1515 class IRCReplyBot(irc.IRCClient):
1517 def connectionMade(self):
1518 self.nickname = self.factory.nickname
1519 irc.IRCClient.connectionMade(self)
1521 def privmsg(self, user, channel, msg):
1522 if user.lower() == channel.lower():
1523 d = self.factory.getUser(msg)
1524 d.addErrback(catchError)
1525 d.addCallback(lambda m: "Status of %s: %s" % (user, m))
1526 d.addCallback(lambda m: self.msg(user, m))
1529 class IIRCClientFactory(components.Interface):
1535 def getUser(self, user):
1536 """Return a deferred returning a string"""
1538 def buildProtocol(self, addr):
1539 """Return a protocol"""
1542 class IRCClientFactoryFromService(protocol.ClientFactory):
1544 __implements__ = IIRCClientFactory,
1546 protocol = IRCReplyBot
1549 def __init__(self, service):
1550 self.service = service
1552 def getUser(self, user):
1553 return self.service.getUser()
1555 components.registerAdapter(IRCClientFactoryFromService, IFingerService)
1558 class UsersModel(model.MethodModel):
1560 def __init__(self, service):
1561 self.service = service
1563 def wmfactory_users(self):
1564 return self.service.getUsers()
1566 components.registerAdapter(UsersModel, IFingerService)
1568 class UserStatusTree(page.Page):
1570 template = """<html><head><title>Users</title><head><body>
1571 <h1>Users</h1>
1572 <ul model="users" view="List">
1573 <li pattern="listItem" /><a view="Link" model="."
1574 href="dummy"><span model="." view="Text" /></a>
1575 </ul></body></html>"""
1577 def initialize(self, **kwargs):
1578 self.putChild('RPC2.0', UserStatusXR(self.model.service))
1580 def getDynamicChild(self, path, request):
1581 return UserStatus(user=path, service=self.model.service)
1583 components.registerAdapter(UserStatusTree, IFingerService)
1586 class UserStatus(page.Page):
1588 template='''<html><head><<title view="Text" model="user"/></heaD>
1589 <body><h1 view="Text" model="user"/>
1590 <p mode="status" view="Text" />
1591 </body></html>'''
1593 def initialize(self, **kwargs):
1594 self.user = kwargs['user']
1595 self.service = kwargs['service']
1597 def wmfactory_user(self):
1600 def wmfactory_status(self):
1601 return self.service.getUser(self.user)
1603 class UserStatusXR(xmlrpc.XMLPRC):
1605 def __init__(self, service):
1606 xmlrpc.XMLRPC.__init__(self)
1607 self.service = service
1609 def xmlrpc_getUser(self, user):
1610 return self.service.getUser(user)
1613 class IPerspectiveFinger(components.Interface):
1615 def remote_getUser(self, username):
1616 """return a user's status"""
1618 def remote_getUsers(self):
1619 """return a user's status"""
1622 class PerspectiveFingerFromService(pb.Root):
1624 __implements__ = IPerspectiveFinger,
1626 def __init__(self, service):
1627 self.service = service
1629 def remote_getUser(self, username):
1630 return self.service.getUser(username)
1632 def remote_getUsers(self):
1633 return self.service.getUsers()
1635 components.registerAdapter(PerspectiveFingerFromService, IFingerService)
1638 class FingerService(app.ApplicationService):
1640 __implements__ = IFingerService,
1642 def __init__(self, file, *args, **kwargs):
1643 app.ApplicationService.__init__(self, *args, **kwargs)
1646 def startService(self):
1647 app.ApplicationService.startService(self)
1652 for line in file(self.file):
1653 user, status = line.split(':', 1)
1654 self.users[user] = status
1655 self.call = reactor.callLater(30, self._read)
1657 def stopService(self):
1658 app.ApplicationService.stopService(self)
1661 def getUser(self, user):
1662 return defer.succeed(self.users.get(u, "No such user"))
1665 return defer.succeed(self.users.keys())
1668 application = app.Application('finger', uid=1, gid=1)
1669 f = FingerService('/etc/users', application, 'finger')
1670 application.listenTCP(79, IFingerFactory(f))
1671 application.listenTCP(80, server.Site(resource.IResource(f)))
1672 i = IIRCClientFactory(f)
1673 i.nickname = 'fingerbot'
1674 application.connectTCP('irc.freenode.org', 6667, i)
1675 application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
1678 <p>We add support for perspective broker, Twisted's native remote
1679 object protocol. Now, Twisted clients will not have to go through
1680 XML-RPCish contortions to get information about users.</p>
1682 <h2>Support HTTPS</h2>
1685 # Do everything properly, and componentize
1686 from twisted.internet import protocol, reactor, defer, app
1687 from twisted.protocols import basic, irc
1688 from twisted.python import components
1689 from twisted.web import resource, server, static, xmlrpc, microdom
1690 from twisted.web.woven import page, widget
1691 from twisted.spread import pb
1692 from OpenSSL import SSL
1695 class IFingerService(components.Interface):
1697 def getUser(self, user):
1698 '''Return a deferred returning a string'''
1701 '''Return a deferred returning a list of strings'''
1703 class IFingerSettingService(components.Interface):
1705 def setUser(self, user, status):
1706 '''Set the user's status to something'''
1708 def catchError(err):
1709 return "Internal error in server"
1712 class FingerProtocol(basic.LineReceiver):
1714 def lineReceived(self, user):
1715 d = self.factory.getUser(user)
1716 d.addErrback(catchError)
1717 def writeValue(value):
1718 self.transport.write(value)
1719 self.transport.loseConnection()
1720 d.addCallback(writeValue)
1723 class IFingerFactory(components.Interface):
1725 def getUser(self, user):
1726 """Return a deferred returning a string""""
1728 def buildProtocol(self, addr):
1729 """Return a protocol returning a string""""
1732 class FingerFactoryFromService(protocol.ServerFactory):
1734 __implements__ = IFingerFactory,
1736 protocol = FingerProtocol
1738 def __init__(self, service):
1739 self.service = service
1741 def getUser(self, user):
1742 return self.service.getUser(user)
1744 components.registerAdapter(FingerFactoryFromService, IFingerService)
1747 class FingerSetterProtocol(basic.LineReceiver):
1749 def connectionMade(self):
1752 def lineReceived(self, line):
1753 self.lines.append(line)
1755 def connectionLost(self):
1756 if len(self.lines) == 2:
1757 self.factory.setUser(*self.lines)
1760 class IFingerSetterFactory(components.Interface):
1762 def setUser(self, user, status):
1763 """Return a deferred returning a string"""
1765 def buildProtocol(self, addr):
1766 """Return a protocol returning a string"""
1769 class FingerSetterFactoryFromService(protocol.ServerFactory):
1771 __implements__ = IFingerSetterFactory,
1773 protocol = FingerSetterProtocol
1775 def __init__(self, service):
1776 self.service = service
1778 def setUser(self, user, status):
1779 self.service.setUser(user, status)
1782 components.registerAdapter(FingerSetterFactoryFromService,
1783 IFingerSettingService)
1785 class IRCReplyBot(irc.IRCClient):
1787 def connectionMade(self):
1788 self.nickname = self.factory.nickname
1789 irc.IRCClient.connectionMade(self)
1791 def privmsg(self, user, channel, msg):
1792 if user.lower() == channel.lower():
1793 d = self.factory.getUser(msg)
1794 d.addErrback(catchError)
1795 d.addCallback(lambda m: "Status of %s: %s" % (user, m))
1796 d.addCallback(lambda m: self.msg(user, m))
1799 class IIRCClientFactory(components.Interface):
1805 def getUser(self, user):
1806 """Return a deferred returning a string"""
1808 def buildProtocol(self, addr):
1809 """Return a protocol"""
1812 class IRCClientFactoryFromService(protocol.ClientFactory):
1814 __implements__ = IIRCClientFactory,
1816 protocol = IRCReplyBot
1819 def __init__(self, service):
1820 self.service = service
1822 def getUser(self, user):
1823 return self.service.getUser()
1825 components.registerAdapter(IRCClientFactoryFromService, IFingerService)
1828 class UsersModel(model.MethodModel):
1830 def __init__(self, service):
1831 self.service = service
1833 def wmfactory_users(self):
1834 return self.service.getUsers()
1836 components.registerAdapter(UsersModel, IFingerService)
1838 class UserStatusTree(page.Page):
1840 template = """<html><head><title>Users</title><head><body>
1841 <h1>Users</h1>
1842 <ul model="users" view="List">
1843 <li pattern="listItem" /><a view="Link" model="."
1844 href="dummy"><span model="." view="Text" /></a>
1845 </ul></body></html>"""
1847 def initialize(self, **kwargs):
1848 self.putChild('RPC2.0', UserStatusXR(self.model.service))
1850 def getDynamicChild(self, path, request):
1851 return UserStatus(user=path, service=self.model.service)
1853 components.registerAdapter(UserStatusTree, IFingerService)
1855 class UserStatus(page.Page):
1857 template='''<html><head><title view="Text" model="user"/></heaD>
1858 <body><h1 view="Text" model="user"/>
1859 <p mode="status" view="Text" />
1860 </body></html>'''
1862 def initialize(self, **kwargs):
1863 self.user = kwargs['user']
1864 self.service = kwargs['service']
1866 def wmfactory_user(self):
1869 def wmfactory_status(self):
1870 return self.service.getUser(self.user)
1872 class UserStatusXR(xmlrpc.XMLPRC):
1874 def __init__(self, service):
1875 xmlrpc.XMLRPC.__init__(self)
1876 self.service = service
1878 def xmlrpc_getUser(self, user):
1879 return self.service.getUser(user)
1882 class IPerspectiveFinger(components.Interface):
1884 def remote_getUser(self, username):
1885 """return a user's status"""
1887 def remote_getUsers(self):
1888 """return a user's status"""
1891 class PerspectiveFingerFromService(pb.Root):
1893 __implements__ = IPerspectiveFinger,
1895 def __init__(self, service):
1896 self.service = service
1898 def remote_getUser(self, username):
1899 return self.service.getUser(username)
1901 def remote_getUsers(self):
1902 return self.service.getUsers()
1904 components.registerAdapter(PerspectiveFingerFromService, IFingerService)
1907 class FingerService(app.ApplicationService):
1909 __implements__ = IFingerService,
1911 def __init__(self, file, *args, **kwargs):
1912 app.ApplicationService.__init__(self, *args, **kwargs)
1915 def startService(self):
1916 app.ApplicationService.startService(self)
1921 for line in file(self.file):
1922 user, status = line.split(':', 1)
1923 self.users[user] = status
1924 self.call = reactor.callLater(30, self._read)
1926 def stopService(self):
1927 app.ApplicationService.stopService(self)
1930 def getUser(self, user):
1931 return defer.succeed(self.users.get(u, "No such user"))
1934 return defer.succeed(self.users.keys())
1937 class ServerContextFactory:
1939 def getContext(self):
1940 """Create an SSL context.
1942 This is a sample implementation that loads a certificate from a file
1943 called 'server.pem'."""
1944 ctx = SSL.Context(SSL.SSLv23_METHOD)
1945 ctx.use_certificate_file('server.pem')
1946 ctx.use_privatekey_file('server.pem')
1950 application = app.Application('finger', uid=1, gid=1)
1951 f = FingerService('/etc/users', application, 'finger')
1952 application.listenTCP(79, IFingerFactory(f))
1953 site = server.Site(resource.IResource(f))
1954 application.listenTCP(80, site)
1955 application.listenSSL(443, site, ServerContextFactory())
1956 i = IIRCClientFactory(f)
1957 i.nickname = 'fingerbot'
1958 application.connectTCP('irc.freenode.org', 6667, i)
1959 application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
1962 <p>All we need to do to code an HTTPS site is just write a context
1963 factory (in this case, which loads the certificate from a certain file)
1964 and then use the listenSSL method. Note that one factory (in this
1965 case, a site) can listen on multiple ports with multiple protocols.</p>
1967 <h2>Finger Proxy</h2>
1970 class FingerClient(protocol.Protocol):
1972 def connectionMade(self):
1973 self.transport.write(self.factory.user+"\r\n")
1976 def dataReceived(self, data):
1977 self.buf.append(data)
1979 def connectionLost(self):
1980 self.factory.gotData(''.join(self.buf))
1983 class FingerClientFactory(protocol.ClientFactory):
1985 protocol = FingerClient
1987 def __init__(self, user):
1989 self.d = defer.Deferred()
1991 def clientConnectionFailed(self, _, reason):
1992 self.d.errback(reason)
1994 def gotData(self, data):
1995 self.d.callback(data)
1998 def finger(user, host, port=79):
1999 f = FingerClientFactory(user)
2000 reactor.connectTCP(host, port, f)
2003 class ProxyFingerService(app.ApplicationService):
2004 __implements__ = IFingerService
2006 def getUser(self, user):
2007 user, host = user.split('@', 1)
2008 ret = finger(user, host)
2009 ret.addErrback(lambda _: "Could not connect to remote host")
2013 return defer.succeed([])
2015 application = app.Application('finger', uid=1, gid=1)
2016 f = ProxyFingerService(application, 'finger')
2017 application.listenTCP(79, IFingerFactory(f))
2020 <p>Writing new clients with Twisted is much like writing new servers.
2021 We implement the protocol, which just gathers up all the data, and
2022 give it to the factory. The factory keeps a deferred which is triggered
2023 if the connection either fails or succeeds. When we use the client,
2024 we first make sure the deferred will never fail, by producing a message
2025 in that case. Implementing a wrapper around client which just returns
2026 the deferred is a common pattern. While being less flexible than
2027 using the factory directly, it is also more convenient.</p>
2029 <h2>Organization</h2>
2032 <li>Code belongs in modules: everything above the <code>application=</code>
2034 <li>Templates belong in separate files. The templateFile attribute can be
2035 used to indicate the file.</li>
2036 <li>The templateDirectory attribute will be used to indicate where to look
2041 from twisted.internet import app
2042 from finger import FingerService, IIRCclient, ServerContextFactory, \
2043 IFingerFactory, IPerspectiveFinger
2044 from twisted.web import resource, server
2045 from twisted.spread import pb
2047 application = app.Application('finger', uid=1, gid=1)
2048 f = FingerService('/etc/users', application, 'finger')
2049 application.listenTCP(79, IFingerFactory(f))
2050 r = resource.IResource(f)
2051 r.templateDirectory = '/usr/share/finger/templates/'
2052 site = server.Site(r)
2053 application.listenTCP(80, site)
2054 application.listenSSL(443, site, ServerContextFactory())
2055 i = IIRCClientFactory(f)
2056 i.nickname = 'fingerbot'
2057 application.connectTCP('irc.freenode.org', 6667, i)
2058 application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
2062 <li>Seperaration between: code (module), configuration (file above),
2063 presentation (templates), contents (/etc/users), deployment (twistd)</li>
2064 <li>Examples, early prototypes don't need that.</li>
2065 <li>But when writing correctly, easy to do!</li>
2068 <h2>Easy Configuration</h2>
2070 <p>We can also supply easy configuration for common cases</p>
2073 # in finger.py moudle
2074 def updateApplication(app, **kwargs):
2075 f = FingerService(kwargs['users'], application, 'finger')
2076 application.listenTCP(79, IFingerFactory(f))
2077 r = resource.IResource(f)
2078 r.templateDirectory = kwargs['templates']
2079 site = server.Site(r)
2080 app.listenTCP(80, site)
2081 if kwargs.get('ssl'):
2082 app.listenSSL(443, site, ServerContextFactory())
2083 if kwargs.has_key('ircnick'):
2084 i = IIRCClientFactory(f)
2085 i.nickname = kwargs['ircnick']
2086 ircServer = kwargs['ircserver']
2087 application.connectTCP(ircserver, 6667, i)
2088 if kwargs.has_key('pbport'):
2089 application.listenTCP(int(kwargs['pbport']),
2090 pb.BrokerFactory(IPerspectiveFinger(f))
2093 <p>And we can write simpler files now:</p>
2097 from twisted.internet import app
2100 application = app.Application('finger', uid=1, gid=1)
2101 finger.updateApplication(application,
2103 templatesDirectory='/usr/share/finger/templates',
2105 ircnick='fingerbot',
2106 ircserver='irc.freenode.net',
2111 <p>Note: the finger <em>user</em> still has ultimate power: he can use
2112 updateApplication, or he can use the lower-level interface if he has
2113 specific needs (maybe an ircserver on some other port? maybe we
2114 want the non-ssl webserver to listen only locally? etc. etc.)
2115 This is an important design principle: never force a layer of abstraction:
2116 allow usage of layers of abstractions.</p>
2118 <p>The pasta theory of design:</p>
2121 <li>Spaghetti: each piece of code interacts with every other piece of
2122 code [can be implemented with GOTO, functions, objects]</li>
2123 <li>Lasagna: code has carefully designed layers. Each layer is, in
2124 theory independent. However low-level layers usually cannot be
2125 used easily, and high-level layers depend on low-level layers.</li>
2126 <li>Raviolli: each part of the code is useful by itself. There is a thin
2127 layer of interfaces between various parts [the sauce]. Each part
2128 can be usefully be used elsewhere.</li>
2129 <li>...but sometimes, the user just wants to order "Raviolli", so one
2130 coarse-grain easily definable layer of abstraction on top of it all
2136 <p>So far, the user had to be somewhat of a programmer to use this.
2137 Maybe we can eliminate even that? Move old code to
2138 "finger/service.py", put empty "__init__.py" and...</p>
2142 from twisted.python import usage
2143 from finger import service
2145 class Options(usage.Options):
2148 ['users', 'u', '/etc/users'],
2149 ['templatesDirectory', 't', '/usr/share/finger/templates'],
2150 ['ircnick', 'n', 'fingerbot'],
2151 ['ircserver', None, 'irc.freenode.net'],
2152 ['pbport', 'p', 8889],
2155 optFlags = [['ssl', 's']]
2157 def updateApplication(app, config):
2158 service.updateApplication(app, **config)
2161 <p>And register it all:</p>
2165 register('Finger', 'finger.tap', type='tap', tapname='finger')
2168 <p>And now, the following works</p>
2171 % mktap finger --users=/usr/local/etc/users --ircnick=moshez-finger
2172 % sudo twistd -f finger.tap
2175 <h2>OS Integration</h2>
2177 <p>If we already have the "finger" package installed, we can achieve
2178 easy integration:</p>
2183 % tap2deb --unsigned -m "Foo <foo@example.com>" --type=python finger.tpy
2184 % sudo dpkg -i .build/*.deb
2187 <p>On Red Hat [or Mandrake]</p>
2190 % tap2rpm --type=python finger.tpy #[maybe other options needed]
2191 % sudo rpm -i .build/*.rpm
2194 <p>Will properly register configuration files, init.d sripts, etc. etc.</p>
2196 <p>If it doesn't work on your favourite OS: patches accepted!</p>
2201 <li>Twisted is asynchronous</li>
2202 <li>Twisted has implementations of every useful protocol</li>
2203 <li>In Twisted, implementing new protocols is easy [we just did three]</li>
2204 <li>In Twisted, achieving tight integration of servers and clients
2206 <li>In Twisted, achieving high code usability is easy.</li>
2207 <li>Ease of use of Twisted follows, in a big part, from that of Python.</li>
2208 <li>Bonus: No buffer overflows. Ever. No matter what.</li>
2214 <li>"Twisted is not about forcing. It's about mocking you when you use
2215 the technology in suboptimal ways."</li>
2216 <li>You're not forced to use anything except the reactor...</li>
2217 <li>...not the protocol implementations...</li>
2218 <li>...not application...</li>
2219 <li>...not services...</li>
2220 <li>...not components...</li>
2221 <li>...not woven...</li>
2222 <li>...not perspective broker...</li>
2224 <li>But you should!</li>
2225 <li>Reinventing the wheel is not a good idea, especially if you form
2226 some vaguely squarish lump of glue and poison and try and attach
2227 it to your car.</li>
2228 <li>The Twisted team solved many of the problems you are likely to come
2230 <li>...several times...</li>
2231 <li>...getting it right the nth time.</li>