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