]> code.delx.au - pymsnt/blob - src/ft.py
Use vCard fullname if nickname isn't around.
[pymsnt] / src / ft.py
1 # Copyright 2005 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 from tlib.throttle import Throttler
5 from tlib.xmlw import Element
6 from twisted.internet import protocol
7
8 import disco
9 import lang
10 from debug import LogEvent, INFO, WARN, ERROR
11 import config
12 import utils
13
14 import random
15 import sys
16
17
18 def doRateLimit(setConsumer, consumer):
19 try:
20 rateLimit = int(config.ftRateLimit)
21 except ValueError:
22 rateLimit = 0
23 if rateLimit > 0:
24 throttler = Throttler(consumer, rateLimit)
25 setConsumer(throttler)
26 else:
27 setConsumer(consumer)
28
29 def checkSizeOk(size):
30 try:
31 size = int(size)
32 limit = int(config.ftSizeLimit)
33 except ValueError:
34 return False
35 if limit == 0:
36 return True
37 return limit > size
38
39 ###########
40 # Sending #
41 ###########
42
43 class FTSend:
44 """ For file transfers going from Jabber to MSN. """
45 def __init__(self, session, to, startTransfer, cancelTransfer, filename, filesize):
46 self.startTransfer = startTransfer
47 self.cancelTransfer = cancelTransfer
48 self.filename = filename
49 self.filesize = filesize
50 if not checkSizeOk(self.filesize):
51 LogEvent(INFO, session.jabberID, "File too large.")
52 session.legacycon.sendMessage(to, "", lang.get(session.lang).msnFtSizeRejected % (self.filename, config.ftSizeLimit, config.website), True)
53 self.reject()
54 return
55
56 session.legacycon.sendFile(to, self)
57
58 def accept(self, legacyFileSend):
59 doRateLimit(self.startTransfer, legacyFileSend)
60 self.cleanup()
61
62 def reject(self):
63 self.cancelTransfer()
64 self.cleanup()
65
66 def cleanup(self):
67 del self.startTransfer, self.cancelTransfer
68
69
70 try:
71 from twisted.web import http
72 except ImportError:
73 try:
74 from twisted.protocols import http
75 except ImportError:
76 print "Couldn't find http.HTTPClient. If you're using Twisted 2.0, make sure that you've installed twisted.web"
77 raise
78
79
80 class OOBHeaderHelper(http.HTTPClient):
81 """ Makes a HEAD request and grabs the length """
82 def connectionMade(self):
83 self.sendCommand("HEAD", self.factory.path.encode("utf-8"))
84 self.sendHeader("Host", (self.factory.host + ":" + str(self.factory.port)).encode("utf-8"))
85 self.endHeaders()
86
87 def handleEndHeaders(self):
88 self.factory.gotLength(self.length)
89
90 def handleResponse(self, data):
91 pass
92
93
94 class OOBSendConnector(http.HTTPClient):
95 def connectionMade(self):
96 self.sendCommand("GET", self.factory.path.encode("utf-8"))
97 self.sendHeader("Host", (self.factory.host + ":" + str(self.factory.port)).encode("utf-8"))
98 self.endHeaders()
99 self.first = True
100
101 def handleResponsePart(self, data):
102 self.factory.consumer.write(data)
103
104 def handleResponseEnd(self):
105 # This is called once before writing is finished, and once when the
106 # connection closes. We only consumer.close() on the second.
107 if self.first:
108 self.first = False
109 else:
110 self.factory.consumer.close()
111 self.factory.consumer = None
112 self.factory.finished()
113
114
115
116
117
118 #############
119 # Receiving #
120 #############
121
122 class FTReceive:
123 """ For file transfers going from MSN to Jabber. """
124
125 """
126 Plan of action for this class:
127 * Determine the FT support of the Jabber client.
128 * If we find a common protocol, then send the invitation.
129 * Tell the legacyftp object the result of the invitation.
130 * If it was accepted, then start the transfer.
131
132 """
133
134 def __init__(self, session, senderJID, legacyftp):
135 if not checkSizeOk(legacyftp.filesize):
136 LogEvent(INFO, session.jabberID, "File too large.")
137 legacyftp.reject()
138 session.legacycon.sendMessage(senderJID, "", lang.get(session.lang).msnFtSizeRejected % (legacyftp.filename, config.ftSizeLimit, config.website), False)
139 return
140 self.session = session
141 self.toJID = self.session.jabberID + "/" + self.session.highestResource()
142 self.senderJID = senderJID
143 self.ident = (self.toJID, self.senderJID)
144 self.legacyftp = legacyftp
145 LogEvent(INFO, session.jabberID)
146 self.checkSupport()
147
148 def checkSupport(self):
149 def discoDone(features):
150 LogEvent(INFO, self.ident)
151 enabledS5B = hasattr(self.session.pytrans, "ftSOCKS5Receive")
152 enabledOOB = hasattr(self.session.pytrans, "ftOOBReceive")
153 hasFT = features.count(disco.FT)
154 hasS5B = features.count(disco.S5B)
155 hasOOB = features.count(disco.IQOOB)
156 LogEvent(INFO, self.ident, "Choosing transfer mode.")
157 if hasFT > 0 and hasS5B > 0 and enabledS5B:
158 self.socksMode()
159 elif hasOOB > 0 and enabledOOB:
160 self.oobMode()
161 elif enabledOOB:
162 self.messageOobMode()
163 else:
164 # No support
165 self.legacyftp.reject()
166 del self.legacyftp
167
168 def discoFail(err=None):
169 LogEvent(INFO, self.ident, str(err))
170 if hasattr(self.session.pytrans, "ftOOBReceive"):
171 self.messageOobMode()
172 else:
173 # No support
174 self.legacyftp.reject()
175 del self.legacyftp
176
177 d = disco.DiscoRequest(self.session.pytrans, self.toJID).doDisco()
178 d.addCallbacks(discoDone, discoFail)
179
180 def socksMode(self):
181 def ftReply(el):
182 if el.getAttribute("type") != "result":
183 ftDeclined()
184 return
185 self.session.pytrans.ftSOCKS5Receive.addConnection(utils.socks5Hash(self.sid, self.senderJID, self.toJID), self.legacyftp)
186 LogEvent(INFO, self.ident)
187 iq = Element((None, "iq"))
188 iq.attributes["type"] = "set"
189 iq.attributes["to"] = self.toJID
190 iq.attributes["from"] = self.senderJID
191 query = iq.addElement("query")
192 query.attributes["xmlns"] = disco.S5B
193 query.attributes["sid"] = self.sid
194 query.attributes["mode"] = "tcp"
195 streamhost = query.addElement("streamhost")
196 streamhost.attributes["jid"] = self.senderJID
197 streamhost.attributes["host"] = config.host
198 streamhost.attributes["port"] = config.ftJabberPort
199 d = self.session.pytrans.discovery.sendIq(iq)
200 d.addErrback(ftDeclined) # Timeout
201
202 def ftDeclined(el):
203 self.legacyftp.reject()
204 del self.legacyftp
205
206 LogEvent(INFO, self.ident)
207 self.sid = str(random.randint(1000, sys.maxint))
208 iq = Element((None, "iq"))
209 iq.attributes["type"] = "set"
210 iq.attributes["to"] = self.toJID
211 iq.attributes["from"] = self.senderJID
212 si = iq.addElement("si")
213 si.attributes["xmlns"] = disco.SI
214 si.attributes["profile"] = disco.FT
215 si.attributes["id"] = self.sid
216 file = si.addElement("file")
217 file.attributes["xmlns"] = disco.FT
218 file.attributes["size"] = str(self.legacyftp.filesize)
219 file.attributes["name"] = self.legacyftp.filename
220 # Feature negotiation
221 feature = si.addElement("feature")
222 feature.attributes["xmlns"] = disco.FEATURE_NEG
223 x = feature.addElement("x")
224 x.attributes["xmlns"] = disco.XDATA
225 x.attributes["type"] = "form"
226 field = x.addElement("field")
227 field.attributes["type"] = "list-single"
228 field.attributes["var"] = "stream-method"
229 option = field.addElement("option")
230 value = option.addElement("value")
231 value.addContent(disco.S5B)
232 d = self.session.pytrans.discovery.sendIq(iq, 60*3)
233 d.addCallback(ftReply)
234 d.addErrback(ftDeclined)
235
236 def oobMode(self):
237 def cb(el):
238 if el.getAttribute("type") != "result":
239 self.legacyftp.reject()
240 del self.legacyftp
241 self.session.pytrans.ftOOBReceive.remFile(filename)
242
243 def ecb(ignored=None):
244 self.legacyftp.reject()
245 del self.legacyftp
246
247 LogEvent(INFO, self.ident)
248 filename = self.session.pytrans.ftOOBReceive.putFile(self, self.legacyftp.filename)
249 iq = Element((None, "iq"))
250 iq.attributes["to"] = self.toJID
251 iq.attributes["from"] = self.senderJID
252 query = m.addElement("query")
253 query.attributes["xmlns"] = disco.IQOOB
254 query.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
255 d = self.session.send(iq)
256 d.addCallbacks(cb, ecb)
257
258 def messageOobMode(self):
259 LogEvent(INFO, self.ident)
260 filename = self.session.pytrans.ftOOBReceive.putFile(self, self.legacyftp.filename)
261 m = Element((None, "message"))
262 m.attributes["to"] = self.session.jabberID
263 m.attributes["from"] = self.senderJID
264 m.addElement("body").addContent(config.ftOOBRoot + "/" + filename)
265 x = m.addElement("x")
266 x.attributes["xmlns"] = disco.XOOB
267 x.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
268 self.session.pytrans.send(m)
269
270 def error(self, ignored=None):
271 # FIXME
272 LogEvent(WARN)
273
274
275
276 # SOCKS5
277
278 from tlib import socks5
279 import struct
280
281 class JEP65ConnectionSend(protocol.Protocol):
282 # TODO, clean up and move this to tlib.socks5
283 STATE_INITIAL = 1
284 STATE_WAIT_AUTHOK = 2
285 STATE_WAIT_CONNECTOK = 3
286 STATE_READY = 4
287
288 def __init__(self):
289 self.state = self.STATE_INITIAL
290 self.buf = ""
291
292 def connectionMade(self):
293 self.transport.write(struct.pack("!BBB", 5, 1, 0))
294 self.state = self.STATE_WAIT_AUTHOK
295
296 def connectionLost(self, reason):
297 if self.state == self.STATE_READY:
298 self.factory.consumer.close()
299
300 def _waitAuthOk(self):
301 ver, method = struct.unpack("!BB", self.buf[:2])
302 if ver != 5 or method != 0:
303 self.transport.loseConnection()
304 return
305 self.buf = self.buf[2:] # chop
306
307 # Send CONNECT request
308 length = len(self.factory.hash)
309 self.transport.write(struct.pack("!BBBBB", 5, 1, 0, 3, length))
310 self.transport.write("".join([struct.pack("!B" , ord(x))[0] for x in self.factory.hash]))
311 self.transport.write(struct.pack("!H", 0))
312 self.state = self.STATE_WAIT_CONNECTOK
313
314 def _waitConnectOk(self):
315 ver, rep, rsv, atyp = struct.unpack("!BBBB", self.buf[:4])
316 if not (ver == 5 and rep == 0):
317 self.transport.loseConnection()
318 return
319
320 self.state = self.STATE_READY
321 self.factory.madeConnection(self.transport.addr[0])
322
323 def dataReceived(self, buf):
324 if self.state == self.STATE_READY:
325 self.factory.consumer.write(buf)
326
327 self.buf += buf
328 if self.state == self.STATE_WAIT_AUTHOK:
329 self._waitAuthOk()
330 elif self.state == self.STATE_WAIT_CONNECTOK:
331 self._waitConnectOk()
332
333
334 class JEP65ConnectionReceive(socks5.SOCKSv5):
335 def __init__(self, listener):
336 socks5.SOCKSv5.__init__(self)
337 self.listener = listener
338 self.supportedAuthMechs = [socks5.AUTHMECH_ANON]
339 self.supportedAddrs = [socks5.ADDR_DOMAINNAME]
340 self.enabledCommands = [socks5.CMD_CONNECT]
341 self.addr = ""
342
343 def connectRequested(self, addr, port):
344 # So that the legacyftp can close the connection
345 self.transport.close = self.transport.loseConnection
346
347 # Check for special connect to the namespace -- this signifies that
348 # the client is just checking that it can connect to the streamhost
349 if addr == disco.S5B:
350 self.connectCompleted(addr, 0)
351 self.transport.loseConnection()
352 return
353
354 self.addr = addr
355
356 if self.listener.isActive(addr):
357 self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED)
358 return
359
360 if self.listener.addConnection(addr, self):
361 self.connectCompleted(addr, 0)
362 else:
363 self.sendErrorReply(socks5.REPLY_CONN_REFUSED)
364
365 def connectionLost(self, reason):
366 if self.state == socks5.STATE_CONNECT_PENDING:
367 self.listener.removePendingConnection(self.addr, self)
368 else:
369 self.transport.unregisterProducer()
370 if self.peersock != None:
371 self.peersock.peersock = None
372 self.peersock.transport.unregisterProducer()
373 self.peersock = None
374 self.listener.removeActiveConnection(self.addr)
375
376 class Proxy65(protocol.Factory):
377 def __init__(self, port):
378 LogEvent(INFO)
379 reactor.listenTCP(port, self)
380 self.pendingConns = {}
381 self.activeConns = {}
382
383 def buildProtocol(self, addr):
384 return JEP65ConnectionReceive(self)
385
386 def isActive(self, address):
387 return address in self.activeConns
388
389 def activateStream(self, address):
390 if address in self.pendingConns:
391 olist = self.pendingConns[address]
392 if len(olist) != 2:
393 LogEvent(WARN, '', "Not exactly two!")
394 return
395
396 assert address not in self.activeConns
397 self.activeConns[address] = None
398
399 if not isinstance(olist[0], JEP65ConnectionReceive):
400 legacyftp = olist[0]
401 connection = olist[1]
402 elif not isinstance(olist[1], JEP65ConnectionReceive):
403 legacyftp = olist[1]
404 connection = olist[0]
405 else:
406 LogEvent(WARN, '', "No JEP65Connection")
407 return
408
409 doRateLimit(legacyftp.accept, connection.transport)
410 else:
411 LogEvent(WARN, '', "No pending connection.")
412
413 def addConnection(self, address, connection):
414 olist = self.pendingConns.get(address, [])
415 if len(olist) <= 1:
416 olist.append(connection)
417 self.pendingConns[address] = olist
418 if len(olist) == 2:
419 self.activateStream(address)
420 return True
421 else:
422 return False
423
424 def removePendingConnection(self, address, connection):
425 olist = self.pendingConns[address]
426 if len(olist) == 1:
427 del self.pendingConns[address]
428 else:
429 olist.remove(connection)
430
431 def removeActiveConnection(self, address):
432 del self.activeConns[address]
433
434
435 # OOB download server
436
437 from twisted.web import server, resource, error
438 from twisted.internet import reactor
439
440 from debug import LogEvent, INFO, WARN, ERROR
441
442 class OOBReceiveConnector:
443 def __init__(self, ftReceive, ftHttpPush):
444 self.ftReceive, self.ftHttpPush = ftReceive, ftHttpPush
445 doRateLimit(self.ftReceive.legacyftp.accept, self)
446
447 def write(self, data):
448 self.ftHttpPush.write(data)
449
450 def close(self):
451 self.ftHttpPush.finish()
452
453 def error(self):
454 self.ftHttpPush.finish()
455 self.ftReceive.error()
456
457 class FileTransferOOBReceive(resource.Resource):
458 def __init__(self, port):
459 LogEvent(INFO)
460 self.isLeaf = True
461 self.files = {}
462 self.oobSite = server.Site(self)
463 reactor.listenTCP(port, self.oobSite)
464
465 def putFile(self, file, filename):
466 path = str(random.randint(100000000, 999999999))
467 filename = (path + "/" + filename).replace("//", "/")
468 self.files[filename] = file
469 return filename
470
471 def remFile(self, filename):
472 if self.files.has_key(filename):
473 del self.files[filename]
474
475 def render_GET(self, request):
476 filename = request.path[1:] # Remove the leading /
477 if self.files.has_key(filename):
478 file = self.files[filename]
479 request.setHeader("Content-Length", str(file.legacyftp.filesize))
480 request.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp.filename.encode("utf-8"))
481 OOBReceiveConnector(file, request)
482 del self.files[filename]
483 return server.NOT_DONE_YET
484 else:
485 page = error.NoResource(message="404 File Not Found")
486 return page.render(request)
487
488 def render_HEAD(self, request):
489 filename = request.path[1:] # Remove the leading /
490 if self.files.has_key(filename):
491 file = self.files[filename]
492 request.setHeader("Content-Length", str(file.legacyftp.filesize))
493 request.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp.filename.encode("utf-8"))
494 return ""
495 else:
496 page = error.NoResource(message="404 File Not Found")
497 return page.render(request)
498
499