]> code.delx.au - pymsnt/blob - src/misciq.py
66f87b95903d9849424b1f9f2fa5db3d99eb2dce
[pymsnt] / src / misciq.py
1 # Copyright 2004-2005 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 import utils
5 from twisted.internet import reactor, task, protocol, error
6 from tlib.xmlw import Element, jid
7 from debug import LogEvent, INFO, WARN, ERROR
8 import jabw
9 import legacy
10 import disco
11 import config
12 import lang
13 import ft
14 import base64
15 import sys, urllib
16
17
18 class ConnectUsers:
19 def __init__(self, pytrans):
20 self.pytrans = pytrans
21 self.pytrans.adHocCommands.addCommand("connectusers", self.incomingIq, "command_ConnectUsers")
22
23 def sendProbes(self):
24 for jid in self.pytrans.xdb.files():
25 jabw.sendPresence(self.pytrans, jid, config.jid, ptype="probe")
26
27 def incomingIq(self, el):
28 to = el.getAttribute("from")
29 ID = el.getAttribute("id")
30 ulang = utils.getLang(el)
31
32 if config.admins.count(jid.intern(to).userhost()) == 0:
33 self.pytrans.discovery.sendIqError(to=to, fro=config.jid, ID=ID, xmlns=disco.COMMANDS, etype="cancel", condition="not-authorized")
34 return
35
36
37 self.sendProbes()
38
39 iq = Element((None, "iq"))
40 iq.attributes["to"] = to
41 iq.attributes["from"] = config.jid
42 if(ID):
43 iq.attributes["id"] = ID
44 iq.attributes["type"] = "result"
45
46 command = iq.addElement("command")
47 command.attributes["sessionid"] = self.pytrans.makeMessageID()
48 command.attributes["xmlns"] = disco.COMMANDS
49 command.attributes["status"] = "completed"
50
51 x = command.addElement("x")
52 x.attributes["xmlns"] = disco.XDATA
53 x.attributes["type"] = "result"
54
55 title = x.addElement("title")
56 title.addContent(lang.get(ulang).command_ConnectUsers)
57
58 field = x.addElement("field")
59 field.attributes["type"] = "fixed"
60 field.addElement("value").addContent(lang.get(ulang).command_Done)
61
62 self.pytrans.send(iq)
63
64
65 class Statistics:
66 def __init__(self, pytrans):
67 self.pytrans = pytrans
68 self.pytrans.adHocCommands.addCommand("stats", self.incomingIq, "command_Statistics")
69
70 # self.stats is indexed by a unique ID, with value being the value for that statistic
71 self.stats = {}
72 self.stats["Uptime"] = 0
73 self.stats["OnlineUsers"] = 0
74 self.stats["TotalUsers"] = 0
75
76 legacy.startStats(self)
77
78 def incomingIq(self, el):
79 to = el.getAttribute("from")
80 ID = el.getAttribute("id")
81 ulang = utils.getLang(el)
82
83 iq = Element((None, "iq"))
84 iq.attributes["to"] = to
85 iq.attributes["from"] = config.jid
86 if(ID):
87 iq.attributes["id"] = ID
88 iq.attributes["type"] = "result"
89
90 command = iq.addElement("command")
91 command.attributes["sessionid"] = self.pytrans.makeMessageID()
92 command.attributes["xmlns"] = disco.COMMANDS
93 command.attributes["status"] = "completed"
94
95 x = command.addElement("x")
96 x.attributes["xmlns"] = disco.XDATA
97 x.attributes["type"] = "result"
98
99 title = x.addElement("title")
100 title.addContent(lang.get(ulang).command_Statistics)
101
102 for key in self.stats:
103 label = getattr(lang.get(ulang), "command_%s" % key)
104 description = getattr(lang.get(ulang), "command_%s_Desc" % key)
105 field = x.addElement("field")
106 field.attributes["var"] = key
107 field.attributes["label"] = label
108 field.attributes["type"] = "text-single"
109 field.addElement("value").addContent(str(self.stats[key]))
110 field.addElement("desc").addContent(description)
111
112 self.pytrans.send(iq)
113
114
115
116 class AdHocCommands:
117 def __init__(self, pytrans):
118 self.pytrans = pytrans
119 self.pytrans.discovery.addFeature(disco.COMMANDS, self.incomingIq, config.jid)
120 self.pytrans.discovery.addNode(disco.COMMANDS, self.sendCommandList, "command_CommandList", config.jid, True)
121
122 self.commands = {} # Dict of handlers indexed by node
123 self.commandNames = {} # Dict of names indexed by node
124
125 def addCommand(self, command, handler, name):
126 self.commands[command] = handler
127 self.commandNames[command] = name
128 self.pytrans.discovery.addNode(command, self.incomingIq, name, config.jid, False)
129
130 def incomingIq(self, el):
131 itype = el.getAttribute("type")
132 fro = el.getAttribute("from")
133 froj = jid.intern(fro)
134 to = el.getAttribute("to")
135 ID = el.getAttribute("id")
136
137 LogEvent(INFO, "", "Looking for handler")
138
139 node = None
140 for child in el.elements():
141 xmlns = child.uri
142 node = child.getAttribute("node")
143
144 handled = False
145 if(child.name == "query" and xmlns == disco.DISCO_INFO):
146 if(node and self.commands.has_key(node) and (itype == "get")):
147 self.sendCommandInfoResponse(to=fro, ID=ID)
148 handled = True
149 elif(child.name == "query" and xmlns == disco.DISCO_ITEMS):
150 if(node and self.commands.has_key(node) and (itype == "get")):
151 self.sendCommandItemsResponse(to=fro, ID=ID)
152 handled = True
153 elif(child.name == "command" and xmlns == disco.COMMANDS):
154 if((node and self.commands.has_key(node)) and (itype == "set" or itype == "error")):
155 self.commands[node](el)
156 handled = True
157 if(not handled):
158 LogEvent(WARN, "", "Unknown Ad-Hoc command received.")
159 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=xmlns, etype="cancel", condition="feature-not-implemented")
160
161
162 def sendCommandList(self, el):
163 to = el.getAttribute("from")
164 ID = el.getAttribute("id")
165 ulang = utils.getLang(el)
166
167 iq = Element((None, "iq"))
168 iq.attributes["to"] = to
169 iq.attributes["from"] = config.jid
170 if ID:
171 iq.attributes["id"] = ID
172 iq.attributes["type"] = "result"
173
174 query = iq.addElement("query")
175 query.attributes["xmlns"] = disco.DISCO_ITEMS
176 query.attributes["node"] = disco.COMMANDS
177
178 for command in self.commands:
179 item = query.addElement("item")
180 item.attributes["jid"] = config.jid
181 item.attributes["node"] = command
182 item.attributes["name"] = getattr(lang.get(ulang), self.commandNames[command])
183
184 self.pytrans.send(iq)
185
186 def sendCommandInfoResponse(self, to, ID):
187 LogEvent(INFO, "", "Replying to disco#info")
188 iq = Element((None, "iq"))
189 iq.attributes["type"] = "result"
190 iq.attributes["from"] = config.jid
191 iq.attributes["to"] = to
192 if(ID): iq.attributes["id"] = ID
193 query = iq.addElement("query")
194 query.attributes["xmlns"] = disco.DISCO_INFO
195
196 feature = query.addElement("feature")
197 feature.attributes["var"] = disco.COMMANDS
198 self.pytrans.send(iq)
199
200 def sendCommandItemsResponse(self, to, ID):
201 LogEvent(INFO, "", "Replying to disco#items")
202 iq = Element((None, "iq"))
203 iq.attributes["type"] = "result"
204 iq.attributes["from"] = config.jid
205 iq.attributes["to"] = to
206 if(ID): iq.attributes["id"] = ID
207 query = iq.addElement("query")
208 query.attributes["xmlns"] = disco.DISCO_ITEMS
209 self.pytrans.send(iq)
210
211
212 class VCardFactory:
213 def __init__(self, pytrans):
214 self.pytrans = pytrans
215 self.pytrans.discovery.addFeature("vcard-temp", self.incomingIq, "USER")
216 self.pytrans.discovery.addFeature("vcard-temp", self.incomingIq, config.jid)
217
218 def incomingIq(self, el):
219 itype = el.getAttribute("type")
220 fro = el.getAttribute("from")
221 froj = jid.intern(fro)
222 to = el.getAttribute("to")
223 toj = jid.intern(to)
224 ID = el.getAttribute("id")
225 if itype != "get" and itype != "error":
226 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="cancel", condition="feature-not-implemented")
227 return
228
229 LogEvent(INFO, "", "Sending vCard")
230
231 toGateway = not (to.find('@') > 0)
232
233 if not toGateway:
234 if not self.pytrans.sessions.has_key(froj.userhost()):
235 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="auth", condition="not-authorized")
236 return
237 s = self.pytrans.sessions[froj.userhost()]
238 if not s.ready:
239 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="auth", condition="not-authorized")
240 return
241
242 c = s.contactList.findContact(toj.userhost())
243 if not c:
244 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="cancel", condition="recipient-unavailable")
245 return
246
247
248 iq = Element((None, "iq"))
249 iq.attributes["to"] = fro
250 iq.attributes["from"] = to
251 if ID:
252 iq.attributes["id"] = ID
253 iq.attributes["type"] = "result"
254 vCard = iq.addElement("vCard")
255 vCard.attributes["xmlns"] = "vcard-temp"
256 if toGateway:
257 FN = vCard.addElement("FN")
258 FN.addContent(config.discoName)
259 DESC = vCard.addElement("DESC")
260 DESC.addContent(config.discoName)
261 URL = vCard.addElement("URL")
262 URL.addContent(legacy.url)
263 else:
264 if c.nickname:
265 NICKNAME = vCard.addElement("NICKNAME")
266 NICKNAME.addContent(c.nickname)
267 if c.avatar:
268 PHOTO = c.avatar.makePhotoElement()
269 vCard.addChild(PHOTO)
270
271 self.pytrans.send(iq)
272
273 class IqAvatarFactory:
274 def __init__(self, pytrans):
275 self.pytrans = pytrans
276 self.pytrans.discovery.addFeature(disco.IQAVATAR, self.incomingIq, "USER")
277 self.pytrans.discovery.addFeature(disco.STORAGEAVATAR, self.incomingIq, "USER")
278
279 def incomingIq(self, el):
280 itype = el.getAttribute("type")
281 fro = el.getAttribute("from")
282 froj = jid.intern(fro)
283 to = el.getAttribute("to")
284 toj = jid.intern(to)
285 ID = el.getAttribute("id")
286
287 if(itype != "get" and itype != "error"):
288 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="cancel", condition="feature-not-implemented")
289 return
290
291 LogEvent(INFO, "", "Retrieving avatar")
292
293 if(not self.pytrans.sessions.has_key(froj.userhost())):
294 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="auth", condition="not-authorized")
295 return
296 s = self.pytrans.sessions[froj.userhost()]
297 if(not s.ready):
298 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="auth", condition="not-authorized")
299 return
300
301 c = s.contactList.findContact(toj.userhost())
302 if(not c):
303 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="cancel", condition="recipient-unavailable")
304 return
305
306 iq = Element((None, "iq"))
307 iq.attributes["to"] = fro
308 iq.attributes["from"] = to
309 if ID:
310 iq.attributes["id"] = ID
311 iq.attributes["type"] = "result"
312 query = iq.addElement("query")
313 query.attributes["xmlns"] = disco.IQAVATAR
314 if(c.avatar):
315 DATA = c.avatar.makeDataElement()
316 query.addChild(DATA)
317
318 self.pytrans.send(iq)
319
320
321
322 class PingService:
323 def __init__(self, pytrans):
324 self.pytrans = pytrans
325 # self.pingCounter = 0
326 # self.pingTask = task.LoopingCall(self.pingCheck)
327 self.pingTask = task.LoopingCall(self.whitespace)
328 # reactor.callLater(10.0, self.start)
329
330 # def start(self):
331 # self.pingTask.start(120.0)
332
333 def whitespace(self):
334 self.pytrans.send(" ")
335
336 # def pingCheck(self):
337 # if(self.pingCounter >= 2 and self.pytrans.xmlstream): # Two minutes of no response from the main server
338 # LogEvent(WARN, "", "Disconnecting because the main server has ignored our pings for too long.")
339 # self.pytrans.xmlstream.transport.loseConnection()
340 # elif(config.mainServerJID):
341 # d = self.pytrans.discovery.sendIq(self.makePingPacket())
342 # d.addCallback(self.pongReceived)
343 # d.addErrback(self.pongFailed)
344 # self.pingCounter += 1
345
346 # def pongReceived(self, el):
347 # self.pingCounter = 0
348
349 # def pongFailed(self, el):
350 # pass
351
352 # def makePingPacket(self):
353 # iq = Element((None, "iq"))
354 # iq.attributes["from"] = config.jid
355 # iq.attributes["to"] = config.mainServerJID
356 # iq.attributes["type"] = "get"
357 # query = iq.addElement("query")
358 # query.attributes["xmlns"] = disco.IQVERSION
359 # return iq
360
361 class GatewayTranslator:
362 def __init__(self, pytrans):
363 self.pytrans = pytrans
364 self.pytrans.discovery.addFeature(disco.IQGATEWAY, self.incomingIq, config.jid)
365
366 def incomingIq(self, el):
367 fro = el.getAttribute("from")
368 ID = el.getAttribute("id")
369 itype = el.getAttribute("type")
370 if(itype == "get"):
371 self.sendPrompt(fro, ID, utils.getLang(el))
372 elif(itype == "set"):
373 self.sendTranslation(fro, ID, el)
374
375
376 def sendPrompt(self, to, ID, ulang):
377 LogEvent(INFO)
378
379 iq = Element((None, "iq"))
380
381 iq.attributes["type"] = "result"
382 iq.attributes["from"] = config.jid
383 iq.attributes["to"] = to
384 if ID:
385 iq.attributes["id"] = ID
386 query = iq.addElement("query")
387 query.attributes["xmlns"] = disco.IQGATEWAY
388 desc = query.addElement("desc")
389 desc.addContent(lang.get(ulang).gatewayTranslator)
390 prompt = query.addElement("prompt")
391
392 self.pytrans.send(iq)
393
394 def sendTranslation(self, to, ID, el):
395 LogEvent(INFO)
396
397 # Find the user's legacy account
398 legacyaccount = None
399 for query in el.elements():
400 if(query.name == "query"):
401 for child in query.elements():
402 if(child.name == "prompt"):
403 legacyaccount = str(child)
404 break
405 break
406
407
408 if(legacyaccount and len(legacyaccount) > 0):
409 LogEvent(INFO, "", "Sending translated account.")
410 iq = Element((None, "iq"))
411 iq.attributes["type"] = "result"
412 iq.attributes["from"] = config.jid
413 iq.attributes["to"] = to
414 if ID:
415 iq.attributes["id"] = ID
416 query = iq.addElement("query")
417 query.attributes["xmlns"] = disco.IQGATEWAY
418 prompt = query.addElement("prompt")
419 prompt.addContent(legacy.translateAccount(legacyaccount))
420
421 self.pytrans.send(iq)
422
423 else:
424 self.pytrans.discovery.sendIqError(to, ID, disco.IQGATEWAY)
425 self.pytrans.discovery.sendIqError(to=to, fro=config.jid, ID=ID, xmlns=disco.IQGATEWAY, etype="retry", condition="bad-request")
426
427
428
429 class VersionTeller:
430 def __init__(self, pytrans):
431 self.pytrans = pytrans
432 self.pytrans.discovery.addFeature(disco.IQVERSION, self.incomingIq, config.jid)
433 self.pytrans.discovery.addFeature(disco.IQVERSION, self.incomingIq, "USER")
434
435 def incomingIq(self, el):
436 eltype = el.getAttribute("type")
437 if(eltype != "get"): return # Only answer "get" stanzas
438
439 self.sendVersion(el)
440
441 def sendVersion(self, el):
442 LogEvent(INFO)
443 iq = Element((None, "iq"))
444 iq.attributes["type"] = "result"
445 iq.attributes["from"] = el.getAttribute("to")
446 iq.attributes["to"] = el.getAttribute("from")
447 if(el.getAttribute("id")):
448 iq.attributes["id"] = el.getAttribute("id")
449 query = iq.addElement("query")
450 query.attributes["xmlns"] = disco.IQVERSION
451 name = query.addElement("name")
452 name.addContent(config.discoName)
453 version = query.addElement("version")
454 version.addContent(legacy.version)
455 os = query.addElement("os")
456 os.addContent("Python" + ".".join([str(x) for x in sys.version_info[0:3]]) + " - " + sys.platform)
457
458 self.pytrans.send(iq)
459
460
461 class FileTransferOOBSend:
462 def __init__(self, pytrans):
463 self.pytrans = pytrans
464 self.pytrans.discovery.addFeature(disco.IQOOB, self.incomingOOB, "USER")
465
466 def incomingOOB(self, el):
467 ID = el.getAttribute("id")
468 def errOut():
469 self.pytrans.discovery.sendIqError(to=el.getAttribute("from"), fro=el.getAttribute("to"), ID=ID, xmlns=disco.IQOOB, etype="cancel", condition="feature-not-implemented")
470
471 if el.attributes["type"] != "set":
472 return errOut()
473 for child in el.elements():
474 if child.name == "query":
475 query = child
476 break
477 else:
478 return errOut()
479 for child in query.elements():
480 if child.name == "url":
481 url = child.__str__()
482 break
483 else:
484 return errOut()
485
486 froj = jid.intern(el.getAttribute("from"))
487 toj = jid.intern(el.getAttribute("to"))
488 session = self.pytrans.sessions.get(froj.userhost(), None)
489 if not session:
490 return errOut()
491
492 res = utils.getURLBits(url, "http")
493 if not res:
494 return errOut()
495 host, port, path, filename = res
496
497
498 def sendResult():
499 iq = Element((None, "iq"))
500 iq.attributes["to"] = froj.full()
501 iq.attributes["from"] = toj.full()
502 iq.attributes["type"] = "result"
503 if ID:
504 iq.attributes["id"] = ID
505 iq.addElement("query").attributes["xmlns"] = "jabber:iq:oob"
506 self.pytrans.send(iq)
507
508 def startTransfer(consumer):
509 factory = protocol.ClientFactory()
510 factory.protocol = ft.OOBSendConnector
511 factory.path = path
512 factory.host = host
513 factory.port = port
514 factory.consumer = consumer
515 factory.finished = sendResult
516 reactor.connectTCP(host, port, factory)
517
518 def doSendFile(length):
519 ft.FTSend(session, toj.userhost(), startTransfer, errOut, filename, length)
520
521 # Make a HEAD request to grab the length of data first
522 factory = protocol.ClientFactory()
523 factory.protocol = ft.OOBHeaderHelper
524 factory.path = path
525 factory.host = host
526 factory.port = port
527 factory.gotLength = doSendFile
528 reactor.connectTCP(host, port, factory)
529
530
531
532 class Socks5FileTransfer:
533 def __init__(self, pytrans):
534 self.pytrans = pytrans
535 self.pytrans.discovery.addFeature(disco.SI, self.incomingSI, "USER")
536 self.pytrans.discovery.addFeature(disco.FT, lambda: None, "USER")
537 self.pytrans.discovery.addFeature(disco.S5B, self.incomingS5B, "USER")
538 self.sessions = {}
539
540 def incomingSI(self, el):
541 ID = el.getAttribute("id")
542 def errOut():
543 self.pytrans.discovery.sendIqError(to=el.getAttribute("from"), fro=el.getAttribute("to"), ID=ID, xmlns=disco.SI, etype="cancel", condition="bad-request")
544
545 toj = jid.intern(el.getAttribute("to"))
546 froj = jid.intern(el.getAttribute("from"))
547 session = self.pytrans.sessions.get(froj.userhost(), None)
548 if not session:
549 return errOut()
550
551 si = el.si
552 if not (si and si.getAttribute("profile") == disco.FT):
553 return errOut()
554 file = si.file
555 if not (file and file.uri == disco.FT):
556 return errOut()
557 try:
558 sid = si["id"]
559 filename = file["name"]
560 filesize = int(file["size"])
561 except KeyError:
562 return errOut()
563 except ValueError:
564 return errOut()
565
566 # Check that we can use socks5 bytestreams
567 feature = si.feature
568 if not (feature and feature.uri == disco.FEATURE_NEG):
569 return errOut()
570 x = feature.x
571 if not (x and x.uri == disco.XDATA):
572 return errOut()
573 field = x.field
574 if not (field and field.getAttribute("var") == "stream-method"):
575 return errOut()
576 for option in field.elements():
577 value = option.value
578 if not value:
579 continue
580 value = value.__str__()
581 if value == disco.S5B:
582 break
583 else:
584 return errOut() # Socks5 bytestreams not supported :(
585
586
587 def startTransfer(consumer):
588 iq = Element((None, "iq"))
589 iq["type"] = "result"
590 iq["to"] = froj.full()
591 iq["from"] = toj.full()
592 iq["id"] = ID
593 si = iq.addElement("si")
594 si["xmlns"] = disco.SI
595 feature = si.addElement("feature")
596 feature["xmlns"] = disco.FEATURE_NEG
597 x = feature.addElement("x")
598 x["xmlns"] = disco.XDATA
599 x["type"] = "submit"
600 field = x.addElement("field")
601 field["var"] = "stream-method"
602 value = field.addElement("value")
603 value.addContent(disco.S5B)
604 self.pytrans.send(iq)
605 self.sessions[(froj.full(), sid)] = consumer
606
607 ft.FTSend(session, toj.userhost(), startTransfer, errOut, filename, filesize)
608
609 def incomingS5B(self, el):
610 ID = el.getAttribute("id")
611 def errOut():
612 self.pytrans.discovery.sendIqError(to=el.getAttribute("from"), fro=el.getAttribute("to"), ID=ID, xmlns=disco.S5B, etype="cancel", condition="item-not-found")
613
614 if el.getAttribute("type") != "set":
615 return errOut()
616
617 toj = jid.intern(el.getAttribute("to"))
618 froj = jid.intern(el.getAttribute("from"))
619
620 query = el.query
621 if not (query and query.getAttribute("mode", "tcp") == "tcp"):
622 return errOut()
623 sid = query.getAttribute("sid")
624 consumer = self.sessions.pop((froj.full(), sid), None)
625 if not consumer:
626 return errOut()
627 streamhosts = []
628 for streamhost in query.elements():
629 if streamhost.name == "streamhost":
630 try:
631 JID = streamhost["jid"]
632 host = streamhost["host"]
633 port = int(streamhost["port"])
634 except ValueError:
635 return errOut()
636 except KeyError:
637 continue
638 streamhosts.append((JID, host, port))
639
640
641 def gotStreamhost(host):
642 for streamhost in streamhosts:
643 if streamhost[1] == host:
644 jid = streamhost[0]
645 break
646 else:
647 LogEvent(WARN)
648 return errOut()
649
650 for connector in factory.connectors:
651 # Stop any other connections
652 try:
653 connector.stopConnecting()
654 except error.NotConnectingError:
655 pass
656
657 iq = Element((None, "iq"))
658 iq["type"] = "result"
659 iq["from"] = toj.full()
660 iq["to"] = froj.full()
661 iq["id"] = ID
662 query = iq.addElement("query")
663 query["xmlns"] = disco.S5B
664 streamhost = query.addElement("streamhost-used")
665 streamhost["jid"] = jid
666 self.pytrans.send(iq)
667
668
669 # Try the streamhosts
670 factory = protocol.ClientFactory()
671 factory.protocol = ft.JEP65ConnectionSend
672 factory.consumer = consumer
673 factory.hash = utils.socks5Hash(sid, froj.full(), toj.full())
674 factory.madeConnection = gotStreamhost
675 factory.connectors = []
676 for streamhost in streamhosts:
677 factory.connectors.append(reactor.connectTCP(streamhost[1], streamhost[2], factory))
678
679