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