]> code.delx.au - pymsnt/blob - src/misciq.py
Socks5 sending supported
[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.defaultUri
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 ID = el.getAttribute("id")
224 if(itype != "get" and itype != "error"):
225 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="cancel", condition="feature-not-implemented")
226 return
227
228 LogEvent(INFO, "", "Sending vCard")
229
230 toGateway = not (to.find('@') > 0)
231
232 if(not toGateway):
233 if(not self.pytrans.sessions.has_key(froj.userhost())):
234 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="auth", condition="not-authorized")
235 return
236 s = self.pytrans.sessions[froj.userhost()]
237 if(not s.ready):
238 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="auth", condition="not-authorized")
239 return
240
241 c = s.contactList.findContact(to)
242 if(not c):
243 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="cancel", condition="recipient-unavailable")
244 return
245
246
247 iq = Element((None, "iq"))
248 iq.attributes["to"] = fro
249 iq.attributes["from"] = to
250 if ID:
251 iq.attributes["id"] = ID
252 iq.attributes["type"] = "result"
253 vCard = iq.addElement("vCard")
254 vCard.attributes["xmlns"] = "vcard-temp"
255 if(toGateway):
256 FN = vCard.addElement("FN")
257 FN.addContent(legacy.name)
258 DESC = vCard.addElement("DESC")
259 DESC.addContent(legacy.name)
260 URL = vCard.addElement("URL")
261 URL.addContent(legacy.url)
262 else:
263 if(c.nickname):
264 NICKNAME = vCard.addElement("NICKNAME")
265 NICKNAME.addContent(c.nickname)
266 if(c.avatar):
267 PHOTO = c.avatar.makePhotoElement()
268 vCard.addChild(PHOTO)
269
270 self.pytrans.send(iq)
271
272 class IqAvatarFactory:
273 def __init__(self, pytrans):
274 self.pytrans = pytrans
275 self.pytrans.discovery.addFeature(disco.IQAVATAR, self.incomingIq, "USER")
276 self.pytrans.discovery.addFeature(disco.STORAGEAVATAR, self.incomingIq, "USER")
277
278 def incomingIq(self, el):
279 itype = el.getAttribute("type")
280 fro = el.getAttribute("from")
281 froj = jid.intern(fro)
282 to = el.getAttribute("to")
283 ID = el.getAttribute("id")
284
285 if(itype != "get" and itype != "error"):
286 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="cancel", condition="feature-not-implemented")
287 return
288
289 LogEvent(INFO, "", "Retrieving avatar")
290
291 if(not self.pytrans.sessions.has_key(froj.userhost())):
292 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="auth", condition="not-authorized")
293 return
294 s = self.pytrans.sessions[froj.userhost()]
295 if(not s.ready):
296 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="auth", condition="not-authorized")
297 return
298
299 c = s.contactList.findContact(to)
300 if(not c):
301 self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="cancel", condition="recipient-unavailable")
302 return
303
304 iq = Element((None, "iq"))
305 iq.attributes["to"] = fro
306 iq.attributes["from"] = to
307 if ID:
308 iq.attributes["id"] = ID
309 iq.attributes["type"] = "result"
310 query = iq.addElement("query")
311 query.attributes["xmlns"] = disco.IQAVATAR
312 if(c.avatar):
313 DATA = c.avatar.makeDataElement()
314 query.addChild(DATA)
315
316 self.pytrans.send(iq)
317
318
319
320 class PingService:
321 def __init__(self, pytrans):
322 self.pytrans = pytrans
323 # self.pingCounter = 0
324 # self.pingTask = task.LoopingCall(self.pingCheck)
325 self.pingTask = task.LoopingCall(self.whitespace)
326 # reactor.callLater(10.0, self.start)
327
328 # def start(self):
329 # self.pingTask.start(120.0)
330
331 def whitespace(self):
332 self.pytrans.send(" ")
333
334 # def pingCheck(self):
335 # if(self.pingCounter >= 2 and self.pytrans.xmlstream): # Two minutes of no response from the main server
336 # LogEvent(WARN, "", "Disconnecting because the main server has ignored our pings for too long.")
337 # self.pytrans.xmlstream.transport.loseConnection()
338 # elif(config.mainServerJID):
339 # d = self.pytrans.discovery.sendIq(self.makePingPacket())
340 # d.addCallback(self.pongReceived)
341 # d.addErrback(self.pongFailed)
342 # self.pingCounter += 1
343
344 # def pongReceived(self, el):
345 # self.pingCounter = 0
346
347 # def pongFailed(self, el):
348 # pass
349
350 # def makePingPacket(self):
351 # iq = Element((None, "iq"))
352 # iq.attributes["from"] = config.jid
353 # iq.attributes["to"] = config.mainServerJID
354 # iq.attributes["type"] = "get"
355 # query = iq.addElement("query")
356 # query.attributes["xmlns"] = disco.IQVERSION
357 # return iq
358
359 class GatewayTranslator:
360 def __init__(self, pytrans):
361 self.pytrans = pytrans
362 self.pytrans.discovery.addFeature(disco.IQGATEWAY, self.incomingIq, config.jid)
363
364 def incomingIq(self, el):
365 fro = el.getAttribute("from")
366 ID = el.getAttribute("id")
367 itype = el.getAttribute("type")
368 if(itype == "get"):
369 self.sendPrompt(fro, ID, utils.getLang(el))
370 elif(itype == "set"):
371 self.sendTranslation(fro, ID, el)
372
373
374 def sendPrompt(self, to, ID, ulang):
375 LogEvent(INFO)
376
377 iq = Element((None, "iq"))
378
379 iq.attributes["type"] = "result"
380 iq.attributes["from"] = config.jid
381 iq.attributes["to"] = to
382 if ID:
383 iq.attributes["id"] = ID
384 query = iq.addElement("query")
385 query.attributes["xmlns"] = disco.IQGATEWAY
386 desc = query.addElement("desc")
387 desc.addContent(lang.get(ulang).gatewayTranslator)
388 prompt = query.addElement("prompt")
389
390 self.pytrans.send(iq)
391
392 def sendTranslation(self, to, ID, el):
393 LogEvent(INFO)
394
395 # Find the user's legacy account
396 legacyaccount = None
397 for query in el.elements():
398 if(query.name == "query"):
399 for child in query.elements():
400 if(child.name == "prompt"):
401 legacyaccount = str(child)
402 break
403 break
404
405
406 if(legacyaccount and len(legacyaccount) > 0):
407 LogEvent(INFO, "", "Sending translated account.")
408 iq = Element((None, "iq"))
409 iq.attributes["type"] = "result"
410 iq.attributes["from"] = config.jid
411 iq.attributes["to"] = to
412 if ID:
413 iq.attributes["id"] = ID
414 query = iq.addElement("query")
415 query.attributes["xmlns"] = disco.IQGATEWAY
416 prompt = query.addElement("prompt")
417 prompt.addContent(legacy.translateAccount(legacyaccount))
418
419 self.pytrans.send(iq)
420
421 else:
422 self.pytrans.discovery.sendIqError(to, ID, disco.IQGATEWAY)
423 self.pytrans.discovery.sendIqError(to=to, fro=config.jid, ID=ID, xmlns=disco.IQGATEWAY, etype="retry", condition="bad-request")
424
425
426
427 class VersionTeller:
428 def __init__(self, pytrans):
429 self.pytrans = pytrans
430 self.pytrans.discovery.addFeature(disco.IQVERSION, self.incomingIq, config.jid)
431 self.pytrans.discovery.addFeature(disco.IQVERSION, self.incomingIq, "USER")
432
433 def incomingIq(self, el):
434 eltype = el.getAttribute("type")
435 if(eltype != "get"): return # Only answer "get" stanzas
436
437 self.sendVersion(el)
438
439 def sendVersion(self, el):
440 LogEvent(INFO)
441 iq = Element((None, "iq"))
442 iq.attributes["type"] = "result"
443 iq.attributes["from"] = el.getAttribute("to")
444 iq.attributes["to"] = el.getAttribute("from")
445 if(el.getAttribute("id")):
446 iq.attributes["id"] = el.getAttribute("id")
447 query = iq.addElement("query")
448 query.attributes["xmlns"] = disco.IQVERSION
449 name = query.addElement("name")
450 name.addContent(legacy.name)
451 version = query.addElement("version")
452 version.addContent(legacy.version)
453 os = query.addElement("os")
454 os.addContent("Python" + ".".join([str(x) for x in sys.version_info[0:3]]) + " - " + sys.platform)
455
456 self.pytrans.send(iq)
457
458
459 class FileTransferOOBSend:
460 def __init__(self, pytrans):
461 self.pytrans = pytrans
462 self.pytrans.discovery.addFeature(disco.IQOOB, self.incomingOOB, "USER")
463
464 def incomingOOB(self, el):
465 ID = el.getAttribute("id")
466 def errOut():
467 self.pytrans.discovery.sendIqError(to=el.getAttribute("from"), fro=el.getAttribute("to"), ID=ID, xmlns=disco.IQOOB, etype="cancel", condition="feature-not-implemented")
468
469 if el.attributes["type"] != "set":
470 return errOut()
471 for child in el.elements():
472 if child.name == "query":
473 query = child
474 break
475 else:
476 return errOut()
477 for child in query.elements():
478 if child.name == "url":
479 url = child.__str__()
480 break
481 else:
482 return errOut()
483
484 froj = jid.intern(el.getAttribute("from"))
485 toj = jid.intern(el.getAttribute("to"))
486 session = self.pytrans.sessions.get(froj.userhost(), None)
487 if not session:
488 return errOut()
489
490 res = utils.getURLBits(url, "http")
491 if not res:
492 return errOut()
493 host, port, path, filename = res
494
495
496 def sendResult():
497 iq = Element((None, "iq"))
498 iq.attributes["to"] = froj.full()
499 iq.attributes["from"] = toj.full()
500 iq.attributes["type"] = "result"
501 if ID:
502 iq.attributes["id"] = ID
503 iq.addElement("query").attributes["xmlns"] = "jabber:iq:oob"
504 self.pytrans.send(iq)
505
506 def startTransfer(consumer):
507 factory = protocol.ClientFactory()
508 factory.protocol = ft.OOBSendConnector
509 factory.path = path
510 factory.host = host
511 factory.port = port
512 factory.consumer = consumer
513 factory.finished = sendResult
514 reactor.connectTCP(host, port, factory)
515
516 def doSendFile(length):
517 session.legacycon.sendFile(toj.userhost(), ft.FTSend(startTransfer, errOut, filename, length))
518
519 # Make a HEAD request to grab the length of data first
520 factory = protocol.ClientFactory()
521 factory.protocol = ft.OOBHeaderHelper
522 factory.path = path
523 factory.host = host
524 factory.port = port
525 factory.gotLength = doSendFile
526 reactor.connectTCP(host, port, factory)
527
528
529
530 class Socks5FileTransfer:
531 def __init__(self, pytrans):
532 self.pytrans = pytrans
533 self.pytrans.discovery.addFeature(disco.SI, self.incomingSI, "USER")
534 self.pytrans.discovery.addFeature(disco.FT, lambda: None, "USER")
535 self.pytrans.discovery.addFeature(disco.S5B, self.incomingS5B, "USER")
536 self.sessions = {}
537
538 def incomingSI(self, el):
539 ID = el.getAttribute("id")
540 def errOut():
541 self.pytrans.discovery.sendIqError(to=el.getAttribute("from"), fro=el.getAttribute("to"), ID=ID, xmlns=disco.SI, etype="cancel", condition="bad-request")
542
543 toj = jid.intern(el.getAttribute("to"))
544 froj = jid.intern(el.getAttribute("from"))
545 session = self.pytrans.sessions.get(froj.userhost(), None)
546 if not session:
547 return errOut()
548
549 si = el.si
550 if not (si and si.getAttribute("profile") == disco.FT):
551 return errOut()
552 file = si.file
553 if not (file and file.defaultUri == disco.FT):
554 return errOut()
555 try:
556 sid = si["id"]
557 filename = file["name"]
558 filesize = int(file["size"])
559 except KeyError:
560 return errOut()
561 except ValueError:
562 return errOut()
563
564 # Check that we can use socks5 bytestreams
565 feature = si.feature
566 if not (feature and feature.defaultUri == disco.FEATURE_NEG):
567 return errOut()
568 x = feature.x
569 if not (x and x.defaultUri == disco.XDATA):
570 return errOut()
571 field = x.field
572 if not (field and field.getAttribute("var") == "stream-method"):
573 return errOut()
574 for option in field.elements():
575 value = option.value
576 if not value:
577 continue
578 value = value.__str__()
579 if value == disco.S5B:
580 break
581 else:
582 return errOut() # Socks5 bytestreams not supported :(
583
584
585 def startTransfer(consumer):
586 iq = Element((None, "iq"))
587 iq["type"] = "result"
588 iq["to"] = froj.full()
589 iq["from"] = toj.full()
590 iq["id"] = ID
591 si = iq.addElement("si")
592 si["xmlns"] = disco.SI
593 feature = si.addElement("feature")
594 feature["xmlns"] = disco.FEATURE_NEG
595 x = feature.addElement("x")
596 x["xmlns"] = disco.XDATA
597 x["type"] = "submit"
598 field = x.addElement("field")
599 field["var"] = "stream-method"
600 value = field.addElement("value")
601 value.addContent(disco.S5B)
602 self.pytrans.send(iq)
603 self.sessions[(froj.full(), sid)] = consumer
604
605 session.legacycon.sendFile(toj.userhost(), ft.FTSend(startTransfer, errOut, filename, filesize))
606
607 def incomingS5B(self, el):
608 ID = el.getAttribute("id")
609 def errOut():
610 self.pytrans.discovery.sendIqError(to=el.getAttribute("from"), fro=el.getAttribute("to"), ID=ID, xmlns=disco.S5B, etype="cancel", condition="item-not-found")
611
612 if el.getAttribute("type") != "set":
613 return errOut()
614
615 toj = jid.intern(el.getAttribute("to"))
616 froj = jid.intern(el.getAttribute("from"))
617
618 query = el.query
619 if not (query and query.getAttribute("mode") == "tcp"):
620 return errOut()
621 sid = query.getAttribute("sid")
622 consumer = self.sessions.pop((froj.full(), sid), None)
623 if not consumer:
624 return errOut()
625 streamhosts = []
626 for streamhost in query.elements():
627 if streamhost.name == "streamhost":
628 try:
629 JID = streamhost["jid"]
630 host = streamhost["host"]
631 port = int(streamhost["port"])
632 except ValueError:
633 return errOut()
634 except KeyError:
635 continue
636 streamhosts.append((JID, host, port))
637
638
639 def gotStreamhost(host):
640 for streamhost in streamhosts:
641 if streamhost[1] == host:
642 jid = streamhost[0]
643 break
644 else:
645 LogEvent(WARN)
646 return errOut()
647
648 for connector in factory.connectors:
649 # Stop any other connections
650 try:
651 connector.stopConnecting()
652 except error.NotConnectingError:
653 pass
654
655 iq = Element((None, "iq"))
656 iq["type"] = "result"
657 iq["from"] = toj.full()
658 iq["to"] = froj.full()
659 iq["id"] = ID
660 query = iq.addElement("query")
661 query["xmlns"] = disco.S5B
662 streamhost = query.addElement("streamhost-used")
663 streamhost["jid"] = jid
664 self.pytrans.send(iq)
665
666
667 # Try the streamhosts
668 factory = protocol.ClientFactory()
669 factory.protocol = ft.JEP65ConnectionSend
670 factory.consumer = consumer
671 factory.hash = utils.socks5Hash(sid, froj.full(), toj.full())
672 factory.madeConnection = gotStreamhost
673 factory.connectors = []
674 for streamhost in streamhosts:
675 factory.connectors.append(reactor.connectTCP(streamhost[1], streamhost[2], factory))
676
677