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