initial commit
[txMCP] / txMCP / MCPProtocol.py
1 """\
2 Copyright (c) 2014 Justin Wind
3
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 copies of the Software, and to permit persons to whom the Software is
9 furnished to do so, subject to the following conditions:
10
11 The above copyright notice and this permission notice shall be included in
12 all copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 THE SOFTWARE.
21
22 ============================
23 MUD Client Protocol, Twisted
24 ============================
25
26 Based on this specification: http://www.moo.mud.org/mcp2/mcp2.html
27
28 """
29 from twisted.protocols import basic
30 from collections import deque
31 from random import choice
32 import logging
33 import string
34 import shlex
35 import re
36
37
38 logger = logging.getLogger(__name__)
39
40
41 class MCPError(Exception):
42 pass
43
44
45 def generateKey(length, chars):
46 """Return a string of length, composed from chars, for use as a key."""
47 return ''.join([choice(chars) for i in range(length)])
48
49
50 def versionGEQ(v1, v2):
51 """Return True if the MCP version string v1 ("2.1", exempli gratia) is
52 greater-than-or-equal to v2."""
53 (v1major, v1minor) = v1.split('.', 1)
54 (v2major, v2minor) = v2.split('.', 1)
55 if int(v1major) > int(v2major):
56 return True
57 if int(v1major) == int(v2major) and int(v1minor) >= int(v2minor):
58 return True
59 return False
60
61
62 def versionMin(v1, v2):
63 """Return the minimum of two MCP version strings."""
64 (v1major, v1minor) = v1.split('.', 1)
65 (v2major, v2minor) = v2.split('.', 1)
66 if int(v1major) < int(v2major):
67 return v1
68 elif int(v2major) < int(v1major):
69 return v2
70 elif int(v1minor) < int(v2minor):
71 return v1
72 return v2
73
74
75 def versionBest(iRange, rRange):
76 """Return the best version common to the two ranges."""
77 (iMin, iMax) = iRange
78 (rMin, rMax) = rRange
79 if versionGEQ(rMax, iMin) and versionGEQ(iMax, rMin):
80 return versionMin(iMax, rMax)
81 return None
82
83
84 class MCPPackage(object):
85 """\
86 Bundle of handlers which make up an MCP package.
87 """
88 packageName = ''
89 versionRange = None
90
91 def __init__(self, mcp):
92 self.mcp = mcp
93 self.version = None
94
95 def attach(self, supportedVersion):
96 """\
97 Invoked when the other end has indicated it will support this package,
98 this method should install the message handlers for the supportedVersion.
99 """
100 self.version = supportedVersion
101 raise NotImplementedError
102
103 def handle(self, message, data):
104 """\
105 Handle a packageName message here.
106 """
107 raise NotImplementedError
108
109 def send(self):
110 """\
111 Send a packageName message here.
112 """
113 msg = 'package-message'
114 self.mcp.sendMessage(msg, None)
115 raise NotImplementedError
116
117
118 class MCPPackageNegotiate(MCPPackage):
119 """Handle 'mcp-negotiate' commands."""
120 packageName = 'mcp-negotiate'
121 versionRange = ("1.0", "2.0")
122
123 def __init__(self, mcp):
124 MCPPackage.__init__(self, mcp)
125 self.negotiated = False
126
127 def attach(self, supportedVersion):
128 """Install support for mcp-negotiate commands."""
129 if supportedVersion is None:
130 # normal packages return here, but negotiate is an integral MCP
131 # package, and needs to be able to bootstrap itself
132 supportedVersion = "2.0"
133 self.version = supportedVersion
134 if versionGEQ(supportedVersion, "2.0"):
135 self.mcp.messageHandlers[self.packageName + '-end'] = self.handleEnd
136 if versionGEQ(supportedVersion, "1.0"):
137 self.mcp.messageHandlers[self.packageName + '-can'] = self.handleCan
138 logger.debug(
139 "attached package '%s' (%s)",
140 self.packageName,
141 supportedVersion)
142 if not versionGEQ(supportedVersion, "2.0"):
143 # version 1.0 does not have an end-of-negotiations, so just pretend
144 self.handleEnd(None, {})
145
146 def proffer(self):
147 """Send the list of packages."""
148 for packageName in self.mcp.packagesCapable:
149 package = self.mcp.packagesCapable[packageName]
150 self.sendCan(package)
151 self.sendEnd()
152
153 def sendEnd(self):
154 """Send the command indicating no more packages."""
155 msg = self.packageName + '-end'
156 if self.version is None and not versionGEQ(self.mcp.version, "2.1"):
157 # pre-negotiation, but MCP version doesn't support this message
158 return
159 if self.version is not None and not versionGEQ(self.version, "2.0"):
160 # fully negotiated, but mcp-negotiate version doesn't support this message
161 return
162 self.mcp.sendMessage(msg, None)
163
164 def handleEnd(self, message, data):
165 """Receive the end of packages command."""
166 self.mcp.negotiated = True
167 logger.debug(
168 "negotiations complete")
169 self.mcp.connectionNegotiated()
170
171 def sendCan(self, package):
172 """Send the command indicating a package is available."""
173 msg = self.packageName + '-can'
174 if self.version is not None and not versionGEQ(self.version, "1.0"):
175 # this should never occur, but hey
176 return
177 self.mcp.sendMessage(msg, {
178 'package': package.packageName,
179 'min-version': package.versionRange[0],
180 'max-version': package.versionRange[1]})
181
182 def handleCan(self, message, data):
183 """Receive an available package notification."""
184 for requiredKey in ('package', 'min-version', 'max-version'):
185 if requiredKey not in data:
186 logger.warning(
187 "ignoring '%s' due to missing key '%s'",
188 message,
189 requiredKey)
190 return
191 if data['package'] not in self.mcp.packagesCapable:
192 logger.debug(
193 "unsupported package '%s'",
194 data['package'])
195 return
196 package = self.mcp.packagesCapable[data['package']]
197
198 supportedVersion = versionBest(
199 (data['min-version'], data['max-version']),
200 package.versionRange)
201 if supportedVersion is None:
202 logger.debug(
203 "no version match for package '%s'",
204 data['package'])
205 return
206
207 package.attach(supportedVersion)
208
209
210 class MCPPackageCord(MCPPackage):
211 """Handle 'mcp-cord' comamnds."""
212 packageName = 'mcp-cord'
213 versionRange = ("1.0", "1.0")
214
215 CORDFORMAT = "%s%d"
216
217 def __init__(self, mcp):
218 MCPPackage.__init__(self, mcp)
219 self.cordNext = 0
220 self.cords = {}
221
222 def attach(self, supportedVersion):
223 """Install support for mcp-cord commands."""
224 if supportedVersion is None:
225 return
226 self.version = supportedVersion
227 if versionGEQ(supportedVersion, "1.0"):
228 self.mcp.messageHandlers[self.packageName + '-open'] = self.handleOpen
229 self.mcp.messageHandlers[self.packageName + '-close'] = self.handleClose
230 self.mcp.messageHandlers[self.packageName] = self.handle
231 logger.debug(
232 "attached package %s (%s)",
233 self.packageName,
234 supportedVersion)
235
236 def sendOpen(self, cordType, cb):
237 """\
238 Open a cord, return the cord id.
239 Callback cb(mcp, type, id, msg, data) will be invoked whenever a cord
240 message on the opened id is received, until that cord id is closed."""
241 msg = self.packageName + '-open'
242 cordID = self.CORDFORMAT % ("I" if self.mcp.initiator else "R", self.cordNext)
243 self.cordNext += 1
244 self.mcp.sendMessage(msg, {
245 '_id': cordID,
246 '_type': cordType})
247 self.cords[cordID] = (cb, cordType)
248 return cordID
249
250 def handleOpen(self, message, data):
251 """"""
252 for requiredKey in ['_id', '_type']:
253 if requiredKey not in data:
254 logger.warning(
255 "'%s' missing required key '%s'",
256 message,
257 requiredKey)
258 return
259 if data['_id'] in self.cords:
260 logger.warning(
261 "'%s' of duplicate cord '%s'",
262 message,
263 data['_id'])
264 return
265 self.cords[data['_id']] = data['_type']
266 logger.debug(
267 "opened cord '%s'",
268 data['_id'])
269
270 def sendClose(self, cordID):
271 """"""
272 msg = self.packageName + '-close'
273 if cordID not in self.cords:
274 logger.warning(
275 "tried to close non-existant cord '%s'",
276 cordID)
277 return
278 self.mcp.sendMessage(msg, {
279 '_id': cordID})
280
281 def handleClose(self, message, data):
282 """"""
283 if '_id' not in data:
284 logger.warning(
285 "'%s' missing required key '%s'",
286 message,
287 '_id')
288 return
289 if data['_id'] not in self.cords:
290 logger.warning(
291 "tried to close non-existant cord '%s'",
292 data['_id'])
293 return
294 del self.cords[data['_id']]
295
296 def send(self, cordID, cordMsg, data=None):
297 """"""
298 msg = self.packageName
299 if data is None:
300 data = {}
301 if '_id' not in self.cords:
302 logger.warning(
303 "could not send to non-existant cord '%s'",
304 cordID)
305 return
306 data['_id'] = cordID
307 data['_message'] = cordMsg
308 self.mcp.sendMessage(msg, data)
309
310 def handle(self, message, data):
311 """"""
312 for requiredKey in ('_id', '_message'):
313 if requiredKey not in data:
314 logger.warning(
315 "'%s' missing required key '%s'",
316 message,
317 requiredKey)
318 return
319 if data['_id'] not in self.cords:
320 logger.warning(
321 "'%s' for non-existant cord '%s'",
322 message,
323 data['_id'])
324 return
325 cordID = data['_id']
326 cordMsg = data['_message']
327 # FIXME: maybe delete _id and _message from data before hitting the
328 # callback, because they aren't part of the message proper?
329 (cordCallback, cordType) = self.cords[cordID]
330 if callable(cordCallback):
331 cordCallback(self.mcp, cordType, cordID, cordMsg, data)
332
333
334 # object is inhereted here so that super() will work,
335 # because Twisted's BaseProtocol is an old-style class
336 class MCP(basic.LineOnlyReceiver, object):
337 """\
338 A line-oriented protocol, supporting out-of-band messages.
339 """
340 VERSION_MIN = "1.0"
341 VERSION_MAX = "2.1"
342
343 MCP_HEADER = '#$#'
344 MCP_ESCAPE = '#$"'
345
346 AUTHKEY_CHARACTERS = string.ascii_lowercase + \
347 string.ascii_uppercase + \
348 string.digits + \
349 "-~`!@#$%^&()=+{}[]|';?/><.,"
350 AUTHKEY_SET = set(AUTHKEY_CHARACTERS)
351 AUTHKEY_LEN = 16
352
353 KEY_CHARACTERS = string.ascii_lowercase + \
354 string.ascii_uppercase + \
355 string.digits + \
356 '-'
357 KEY_SET = set(KEY_CHARACTERS)
358 KEY_LEN = 6
359
360 def __init__(self, initiator=False):
361 """\
362 Create a new MCP protocol.
363
364 If initiator is True, proffer handshake; otherwise respond to it.
365 This only affects the initial handshake.
366 """
367 self.initiator = initiator
368 # Which side of the conversation we are.
369
370 self.version = None
371 # The state of the connection handshake.
372 # This will be set to the running protocol version, once established.
373
374 self.authKey = ''
375 # This connection's authentication key.
376 # Blank until handshake complete.
377
378 self.inProgress = {}
379 # A list of multi-line messages which have not yet been terminated,
380 # indexed by their _data-tag property.
381
382 self.packagesCapable = {}
383 # All the packages we know how to handle.
384
385 self.packagesActive = {}
386 # The packages the remote side proffered via negotiation,
387 # which matched ones we can deal with.
388
389 self.messageHandlers = {}
390 # A dispatch table mapping messages to the package functions which
391 # will handle them.
392
393 self.sendQueue = deque()
394 # A list of lines to transmit once the handshake has been completed.
395
396 self.sendMessageQueue = deque()
397 # A list of messages to transmit once the package negotiations have completed.
398
399 self.negotiated = False
400 # True once package negotiations have completed.
401
402 # register the standard packages
403 self.addPackage(MCPPackageNegotiate)
404 self.addPackage(MCPPackageCord)
405
406 # bootstrap support for the negotiation package
407 self.packagesCapable[MCPPackageNegotiate.packageName].attach(None)
408
409
410 def connectionMade(self):
411 """Send the initiator handshake on connection."""
412
413 self._peer = self.transport.getPeer().host
414
415 logger.debug("connectionMade, peer is '%s", self._peer)
416
417
418 if self.initiator:
419 self.sendMessage('mcp', {
420 'version': MCP.VERSION_MIN,
421 'to': MCP.VERSION_MAX})
422
423
424 def connectionEstablished(self):
425 """Called when the MCP handshake has been completed."""
426 # send our package negotiations
427 self.packagesCapable[MCPPackageNegotiate.packageName].proffer()
428 # and flush our queue of pending normal data
429 while True:
430 try:
431 self.sendLine(self.sendQueue.popleft())
432 except IndexError:
433 break
434
435
436 def connectionNegotiated(self):
437 """Called when MCP package exchange has completed."""
438 logger.debug("connection negotiated, flushing queued messages")
439 while True:
440 try:
441 (message, kvs) = self.sendMessageQueue.popleft()
442 except IndexError:
443 break
444 self.sendMessage(message, kvs)
445
446
447 def addPackage(self, packageClass, *args, **kwargs):
448 """Register a package type as one we are capable of handling."""
449 if not issubclass(packageClass, MCPPackage):
450 raise MCPError("cannot install unknown package type")
451
452 pkg = packageClass(self, *args, **kwargs)
453 self.packagesCapable[pkg.packageName] = pkg
454
455
456 class __InProgress(object):
457 """\
458 An unterminated multi-line stanza, waiting for completion.
459
460 data is kept distinct from multiData to ease checking of collisions.
461 the keys used to store data are all collapsed to lowercase.
462 """
463 def __init__(self, message):
464 self.message = message
465 self.data = {}
466 self.multiData = {}
467
468 def setKey(self, key, data):
469 if key in self.multiData:
470 logger.warning(
471 "ignoring attempt to overwrite multiline key '%s' with single value",
472 key)
473 return
474 self.data[key] = data
475
476 def setMultiKey(self, key, data):
477 if key in self.data:
478 logger.warning(
479 "ignoring attempt to overwrite single value key '%s' with multiline datum",
480 key)
481 return
482 if key not in self.multiData:
483 self.multiData[key] = []
484 if data is not None:
485 self.multiData[key].append(data)
486
487 def allData(self):
488 """Return the combined simple and multikey data."""
489 return dict(self.multiData, **self.data)
490
491 def __multiKeyEnd(self, datatag):
492 if datatag not in self.inProgress:
493 logger.warning(
494 "termination of unknown multi-line stanza '%s'",
495 datatag)
496 return
497
498 self._dispatchMessage(
499 self.inProgress[datatag].message,
500 self.inProgress[datatag].allData())
501 del self.inProgress[datatag]
502
503 def __multiKeyContinue(self, line):
504 try:
505 (datatag, line) = re.split(r'\s+', line, 1)
506 except ValueError:
507 (datatag, line) = (line, '')
508
509 if datatag not in self.inProgress:
510 logger.warning(
511 "continuation of unknown multi-line stanza '%s'",
512 datatag)
513 return
514 inProgress = self.inProgress[datatag]
515
516 try:
517 (key, line) = line.split(': ', 1)
518 except ValueError:
519 (key, line) = (line, '')
520
521 key = key.tolower()
522 if key in inProgress.data:
523 logger.warning(
524 "multi-line stanza '%s' tried to update non-multi-line key '%s'",
525 datatag,
526 key)
527 return
528 if key not in inProgress.multiData:
529 logger.warning(
530 "multi-line stanza '%s' tried to update untracked key '%s'",
531 datatag,
532 key)
533 return
534
535 inProgress.data[key].append(line)
536 self.messageUpdate(datatag, key)
537
538 def __lineParseMCP(self, line):
539 """Process an out-of-band message."""
540
541 line = line[len(MCP.MCP_HEADER):]
542
543 try:
544 (message, line) = re.split(r'\s+', line, 1)
545 except ValueError:
546 (message, line) = (line, '')
547
548 if message == ':': # end of multi-line stanza
549 self.__multiKeyEnd(line)
550
551 elif message == '*': # continuation of multi-line stanza
552 self.__multiKeyContinue(line)
553
554 else: # simple message
555 # "#$#message authkey [k: v [...]]"
556 inProgress = MCP.__InProgress(message)
557 multiline = False
558
559 if self.version:
560 try:
561 (authKey, line) = re.split(r'\s+', line, 1)
562 except ValueError:
563 (authKey, line) = (line, '')
564
565 if authKey != self.authKey:
566 logger.warning(
567 "ignoring message with foreign key '%s'",
568 authKey)
569 return
570
571 lexer = shlex.shlex(line, posix=True)
572 lexer.commenters = ''
573 lexer.quotes = '"'
574 lexer.whitespace_split = True
575 try:
576 for key in lexer:
577 # keys are case-insensitive, normalize here
578 key = key.lower()
579
580 if key[-1] != ':':
581 logger.warning(
582 "message '%s' could not parse key '%s'",
583 message,
584 key)
585 return
586 key = key[:-1]
587
588 if key[0] not in string.ascii_lowercase:
589 logger.warning(
590 "message '%s' ignored due to invalid key '%s'",
591 message,
592 key)
593 return
594 if not set(key).issubset(MCP.KEY_SET):
595 logger.warning(
596 "message '%s' ignored due to invalid key '%s'",
597 message,
598 key)
599 return
600
601 try:
602 value = next(lexer)
603 except StopIteration:
604 logger.warning(
605 "message '%s' has key '%s' without value",
606 message,
607 key)
608 return
609
610 if key[-1] == '*':
611 key = key[:-1]
612 if key in inProgress.multiData or key in inProgress.data:
613 logger.warning(
614 "message '%s' ignoring duplicate key '%s'",
615 message,
616 key)
617 continue
618 inProgress.multiData[key] = []
619 multiline = True
620 else:
621 if key in inProgress.data or key in inProgress.multiData:
622 logger.warning(
623 "message '%s' ignoring duplicate key '%s'",
624 message,
625 key)
626 continue
627 inProgress.data[key] = value
628
629 except ValueError:
630 logger.warning(
631 "message '%s' has unparsable data",
632 message)
633 return
634
635 if multiline:
636 if '_data-tag' not in inProgress.data:
637 logger.warning(
638 "ignoring message with multi-line variables but no _data-tag")
639 return
640 self.inProgress[inProgress.data['_data-tag']] = inProgress
641 self.messageUpdate(inProgress.data['_data-tag'], None)
642 else:
643 self.__dispatchMessage(inProgress.message, inProgress.allData())
644
645
646 def messageUpdate(self, datatag, key):
647 """\
648 Called when a multiline message has received a new line, but has not
649 completed.
650
651 Generally ignorable, but some servers awkwardly use multiline messages
652 as continuous channels.
653 Override this to handle such a beast.
654 """
655 pass
656
657
658 def __dispatchMessage(self, message, data):
659 """Invoke the handler function for a message."""
660 logger.debug(
661 "MCP message: %s %s",
662 message,
663 repr(data))
664
665 # handle handshaking messages directly
666 if message == 'mcp':
667 self.__handshake(message, data)
668 else:
669 if message in self.messageHandlers:
670 self.messageHandlers[message](message, data)
671 else:
672 self.messageReceived(message, data)
673
674
675 def __handshake(self, message, data):
676 """Handle 'mcp' messages, which establish a connection."""
677 if self.version:
678 logger.warning(
679 "ignoring handshake message during established session")
680 return
681
682 if 'version' not in data:
683 logger.warning(
684 "%s did not send enough version information",
685 "responder" if self.initiator else "initiator")
686 return
687 if 'to' not in data:
688 data['to'] = data['version']
689 supportedVersion = versionBest(
690 (MCP.VERSION_MIN, MCP.VERSION_MAX),
691 (data['version'], data['to']))
692 if supportedVersion is None:
693 logger.warning(
694 "handshake failed, incompatible versions")
695 # FIXME: maybe raise exception on this
696 return
697
698 if self.initiator:
699 if 'authentication-key' in data:
700 if not set(data['authentication-key']).issubset(MCP.AUTHKEY_SET):
701 logger.warning(
702 "responder proffered unexpected characters in authentication-key")
703 return
704 self.authKey = data['authentication-key']
705 logger.debug(
706 "client started new session with key '%s'",
707 self.authKey)
708 else:
709 logger.warning(
710 "ignoring message '%s' before session established",
711 message)
712 return
713 else:
714 authKey = generateKey(MCP.AUTHKEY_LEN, MCP.AUTHKEY_CHARACTERS)
715 # send before setting, as handshake message doesn't include authkey
716 self.sendMessage('mcp', {
717 'authentication-key': authKey,
718 'version': MCP.VERSION_MIN,
719 'to': MCP.VERSION_MAX})
720 self.authKey = authKey
721 logger.debug(
722 "established new session (%s) with key '%s'",
723 supportedVersion,
724 authKey)
725
726 self.version = supportedVersion
727 self.connectionEstablished()
728
729
730 def lineReceived(self, line):
731 """Process a received line for MCP messages."""
732 if line.startswith(MCP.MCP_HEADER):
733 self.__lineParseMCP(line)
734 else:
735 if line.startswith(MCP.MCP_ESCAPE):
736 line = line[len(MCP.MCP_ESCAPE):]
737 self.lineReceivedInband(line)
738
739
740 def sendLine(self, line):
741 """
742 Sends a line of normal data.
743 """
744
745 if not self.initiator and self.version is None:
746 self.sendQueue.append(line)
747 return
748
749 if line.startswith((MCP.MCP_HEADER, MCP.MCP_ESCAPE)):
750 line = ''.join([MCP.MCP_ESCAPE, line])
751 super(MCP, self).sendLine(line)
752
753
754 def sendMessage(self, message, kvs=None):
755 """
756 Sends an MCP message, with data.
757 """
758 # FIXME: this is janky
759 # queue non-core messages until after package negotiation
760 if not self.negotiated:
761 if not message.startswith('mcp'):
762 logger.debug(
763 "deferred MCP-send of '%s'",
764 message)
765 self.sendMessageQueue.append((message, kvs))
766 return
767
768 datatag = None
769 msg = []
770 line = [MCP.MCP_HEADER, message]
771 if self.authKey is not '':
772 line.extend([' ', self.authKey])
773 if kvs is not None:
774 for k, v in kvs.iteritems():
775 if isinstance(v, basestring) and '\n' not in v:
776 line.extend([' ', k, ': "', v, '"'])
777 else:
778 if not datatag:
779 datatag = generateKey(MCP.KEY_LEN, MCP.KEY_CHARACTERS)
780 line.extend([' ', k, '*: ""'])
781 if not isinstance(v, basestring):
782 vLines = v
783 else:
784 vLines = v.split('\n')
785 for l in vLines:
786 msg.append(''.join([MCP.MCP_HEADER, '* ', datatag, ' ', k, ': ', l]))
787 msg.insert(0, ''.join(line))
788 for m in msg:
789 super(MCP, self).sendLine(m)
790 logger.debug(
791 "MCP-send: %s",
792 m)
793
794
795 def lineReceivedInband(self, line):
796 """
797 Called when there's a line of normal data to process.
798
799 Override in implementation.
800 """
801 print "in: ", line
802 return
803 raise NotImplementedError
804
805
806 def messageReceived(self, message, data):
807 """
808 Called when there's an otherwise-unhandled MCP message.
809 """
810 logger.warning(
811 "unhandled message '%s' %s",
812 message,
813 repr(data))
814 pass
815
816
817 if __name__ == '__main__':
818 from twisted.internet import reactor
819 from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol
820 import sys, os.path
821
822 logging.basicConfig(level=logging.DEBUG)
823
824 if len(sys.argv) < 3:
825 print "Usage: %s <host> <port>" % (os.path.basename(sys.argv[0]))
826 sys.exit(64)
827
828 HOST = sys.argv[1]
829 PORT = int(sys.argv[2])
830
831 def gotMCP(p):
832 print 'got'
833 # p.sendLine("WHO")
834 # reactor.callLater(1, p.sendLine("QUIT"))
835 # reactor.callLater(2, p.transport.loseConnection)
836
837 print "establishing endpoing"
838 point = TCP4ClientEndpoint(reactor, HOST, PORT)
839 print "connecting"
840 d = connectProtocol(point, MCP())
841 print "adding things"
842 d.addCallback(gotMCP)
843 print "running"
844 reactor.run()