]> code.delx.au - pymsnt/blob - src/ft.py
Fixed no file transfer method error when disco fails.
[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 try:
19 size = int(size)
20 limit = int(config.ftSizeLimit)
21 except ValueError:
22 return False
23 if limit == 0:
24 return True
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 if hasattr(self.session.pytrans, "ftOOBReceive"):
155 self.messageOobMode()
156 else:
157 # No support
158 self.legacyftp.reject()
159 del self.legacyftp
160
161 d = disco.DiscoRequest(self.session.pytrans, self.toJID).doDisco()
162 d.addCallbacks(discoDone, discoFail)
163
164 def socksMode(self):
165 def ftReply(el):
166 if el.getAttribute("type") != "result":
167 ftDeclined()
168 return
169 self.session.pytrans.ftSOCKS5Receive.addConnection(utils.socks5Hash(self.sid, self.senderJID, self.toJID), self.legacyftp)
170 LogEvent(INFO, self.ident)
171 iq = Element((None, "iq"))
172 iq.attributes["type"] = "set"
173 iq.attributes["to"] = self.toJID
174 iq.attributes["from"] = self.senderJID
175 query = iq.addElement("query")
176 query.attributes["xmlns"] = disco.S5B
177 query.attributes["sid"] = self.sid
178 query.attributes["mode"] = "tcp"
179 streamhost = query.addElement("streamhost")
180 streamhost.attributes["jid"] = self.senderJID
181 streamhost.attributes["host"] = config.host
182 streamhost.attributes["port"] = config.ftJabberPort
183 d = self.session.pytrans.discovery.sendIq(iq)
184 d.addErrback(ftDeclined) # Timeout
185
186 def ftDeclined(el):
187 self.legacyftp.reject()
188 del self.legacyftp
189
190 LogEvent(INFO, self.ident)
191 self.sid = str(random.randint(1000, sys.maxint))
192 iq = Element((None, "iq"))
193 iq.attributes["type"] = "set"
194 iq.attributes["to"] = self.toJID
195 iq.attributes["from"] = self.senderJID
196 si = iq.addElement("si")
197 si.attributes["xmlns"] = disco.SI
198 si.attributes["profile"] = disco.FT
199 si.attributes["id"] = self.sid
200 file = si.addElement("file")
201 file.attributes["xmlns"] = disco.FT
202 file.attributes["size"] = str(self.legacyftp.filesize)
203 file.attributes["name"] = self.legacyftp.filename
204 # Feature negotiation
205 feature = si.addElement("feature")
206 feature.attributes["xmlns"] = disco.FEATURE_NEG
207 x = feature.addElement("x")
208 x.attributes["xmlns"] = disco.XDATA
209 x.attributes["type"] = "form"
210 field = x.addElement("field")
211 field.attributes["type"] = "list-single"
212 field.attributes["var"] = "stream-method"
213 option = field.addElement("option")
214 value = option.addElement("value")
215 value.addContent(disco.S5B)
216 d = self.session.pytrans.discovery.sendIq(iq, 60*3)
217 d.addCallback(ftReply)
218 d.addErrback(ftDeclined)
219
220 def oobMode(self):
221 def cb(el):
222 if el.getAttribute("type") != "result":
223 self.legacyftp.reject()
224 del self.legacyftp
225 self.session.pytrans.ftOOBReceive.remFile(filename)
226
227 def ecb(ignored=None):
228 self.legacyftp.reject()
229 del self.legacyftp
230
231 LogEvent(INFO, self.ident)
232 filename = self.session.pytrans.ftOOBReceive.putFile(self, self.legacyftp.filename)
233 iq = Element((None, "iq"))
234 iq.attributes["to"] = self.toJID
235 iq.attributes["from"] = self.senderJID
236 query = m.addElement("query")
237 query.attributes["xmlns"] = disco.IQOOB
238 query.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
239 d = self.session.send(iq)
240 d.addCallbacks(cb, ecb)
241
242 def messageOobMode(self):
243 LogEvent(INFO, self.ident)
244 filename = self.session.pytrans.ftOOBReceive.putFile(self, self.legacyftp.filename)
245 m = Element((None, "message"))
246 m.attributes["to"] = self.session.jabberID
247 m.attributes["from"] = self.senderJID
248 m.addElement("body").addContent(config.ftOOBRoot + "/" + filename)
249 x = m.addElement("x")
250 x.attributes["xmlns"] = disco.XOOB
251 x.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
252 self.session.pytrans.send(m)
253
254 def error(self, ignored=None):
255 # FIXME
256 LogEvent(WARN)
257
258
259
260 # SOCKS5
261
262 from tlib import socks5
263 import struct
264
265 class JEP65ConnectionSend(protocol.Protocol):
266 # TODO, clean up and move this to tlib.socks5
267 STATE_INITIAL = 1
268 STATE_WAIT_AUTHOK = 2
269 STATE_WAIT_CONNECTOK = 3
270 STATE_READY = 4
271
272 def __init__(self):
273 self.state = self.STATE_INITIAL
274 self.buf = ""
275
276 def connectionMade(self):
277 self.transport.write(struct.pack("!BBB", 5, 1, 0))
278 self.state = self.STATE_WAIT_AUTHOK
279
280 def connectionLost(self, reason):
281 if self.state == self.STATE_READY:
282 self.factory.consumer.close()
283
284 def _waitAuthOk(self):
285 ver, method = struct.unpack("!BB", self.buf[:2])
286 if ver != 5 or method != 0:
287 self.transport.loseConnection()
288 return
289 self.buf = self.buf[2:] # chop
290
291 # Send CONNECT request
292 length = len(self.factory.hash)
293 self.transport.write(struct.pack("!BBBBB", 5, 1, 0, 3, length))
294 self.transport.write("".join([struct.pack("!B" , ord(x))[0] for x in self.factory.hash]))
295 self.transport.write(struct.pack("!H", 0))
296 self.state = self.STATE_WAIT_CONNECTOK
297
298 def _waitConnectOk(self):
299 ver, rep, rsv, atyp = struct.unpack("!BBBB", self.buf[:4])
300 if not (ver == 5 and rep == 0):
301 self.transport.loseConnection()
302 return
303
304 self.state = self.STATE_READY
305 self.factory.madeConnection(self.transport.addr[0])
306
307 def dataReceived(self, buf):
308 if self.state == self.STATE_READY:
309 self.factory.consumer.write(buf)
310
311 self.buf += buf
312 if self.state == self.STATE_WAIT_AUTHOK:
313 self._waitAuthOk()
314 elif self.state == self.STATE_WAIT_CONNECTOK:
315 self._waitConnectOk()
316
317
318 class JEP65ConnectionReceive(socks5.SOCKSv5):
319 def __init__(self, listener):
320 socks5.SOCKSv5.__init__(self)
321 self.listener = listener
322 self.supportedAuthMechs = [socks5.AUTHMECH_ANON]
323 self.supportedAddrs = [socks5.ADDR_DOMAINNAME]
324 self.enabledCommands = [socks5.CMD_CONNECT]
325 self.addr = ""
326
327 def connectRequested(self, addr, port):
328 # So that the legacyftp can close the connection
329 self.transport.close = self.transport.loseConnection
330
331 # Check for special connect to the namespace -- this signifies that
332 # the client is just checking that it can connect to the streamhost
333 if addr == disco.S5B:
334 self.connectCompleted(addr, 0)
335 self.transport.loseConnection()
336 return
337
338 self.addr = addr
339
340 if self.listener.isActive(addr):
341 self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED)
342 return
343
344 if self.listener.addConnection(addr, self):
345 self.connectCompleted(addr, 0)
346 else:
347 self.sendErrorReply(socks5.REPLY_CONN_REFUSED)
348
349 def connectionLost(self, reason):
350 if self.state == socks5.STATE_CONNECT_PENDING:
351 self.listener.removePendingConnection(self.addr, self)
352 else:
353 self.transport.unregisterProducer()
354 if self.peersock != None:
355 self.peersock.peersock = None
356 self.peersock.transport.unregisterProducer()
357 self.peersock = None
358 self.listener.removeActiveConnection(self.addr)
359
360 class Proxy65(protocol.Factory):
361 def __init__(self, port):
362 LogEvent(INFO)
363 reactor.listenTCP(port, self)
364 self.pendingConns = {}
365 self.activeConns = {}
366
367 def buildProtocol(self, addr):
368 return JEP65ConnectionReceive(self)
369
370 def isActive(self, address):
371 return address in self.activeConns
372
373 def activateStream(self, address):
374 if address in self.pendingConns:
375 olist = self.pendingConns[address]
376 if len(olist) != 2:
377 LogEvent(WARN, '', "Not exactly two!")
378 return
379
380 assert address not in self.activeConns
381 self.activeConns[address] = None
382
383 if not isinstance(olist[0], (JEP65ConnectionReceive, JEP65ConnectionSend)):
384 legacyftp = olist[0]
385 connection = olist[1]
386 elif not isinstance(olist[1], (JEP65ConnectionReceive, JEP65ConnectionSend)):
387 legacyftp = olist[1]
388 connection = olist[0]
389 else:
390 LogEvent(WARN, '', "No JEP65Connection")
391 return
392
393 legacyftp.accept(connection.transport)
394 else:
395 LogEvent(WARN, '', "No pending connection.")
396
397 def addConnection(self, address, connection):
398 olist = self.pendingConns.get(address, [])
399 if len(olist) <= 1:
400 olist.append(connection)
401 self.pendingConns[address] = olist
402 if len(olist) == 2:
403 self.activateStream(address)
404 return True
405 else:
406 return False
407
408 def removePendingConnection(self, address, connection):
409 olist = self.pendingConns[address]
410 if len(olist) == 1:
411 del self.pendingConns[address]
412 else:
413 olist.remove(connection)
414
415 def removeActiveConnection(self, address):
416 del self.activeConns[address]
417
418
419 # OOB download server
420
421 from twisted.web import server, resource, error
422 from twisted.internet import reactor
423
424 from debug import LogEvent, INFO, WARN, ERROR
425
426 class OOBReceiveConnector:
427 def __init__(self, ftReceive, ftHttpPush):
428 self.ftReceive, self.ftHttpPush = ftReceive, ftHttpPush
429 self.ftReceive.legacyftp.accept(self)
430
431 def write(self, data):
432 self.ftHttpPush.write(data)
433
434 def close(self):
435 self.ftHttpPush.finish()
436
437 def error(self):
438 self.ftHttpPush.finish()
439 self.ftReceive.error()
440
441 class FileTransferOOBReceive(resource.Resource):
442 def __init__(self, port):
443 LogEvent(INFO)
444 self.isLeaf = True
445 self.files = {}
446 self.oobSite = server.Site(self)
447 reactor.listenTCP(port, self.oobSite)
448
449 def putFile(self, file, filename):
450 path = str(random.randint(100000000, 999999999))
451 filename = (path + "/" + filename).replace("//", "/")
452 self.files[filename] = file
453 return filename
454
455 def remFile(self, filename):
456 if self.files.has_key(filename):
457 del self.files[filename]
458
459 def render_GET(self, request):
460 filename = request.path[1:] # Remove the leading /
461 if self.files.has_key(filename):
462 file = self.files[filename]
463 request.setHeader("Content-Length", str(file.legacyftp.filesize))
464 request.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp.filename.encode("utf-8"))
465 OOBReceiveConnector(file, request)
466 del self.files[filename]
467 return server.NOT_DONE_YET
468 else:
469 page = error.NoResource(message="404 File Not Found")
470 return page.render(request)
471
472 def render_HEAD(self, request):
473 filename = request.path[1:] # Remove the leading /
474 if self.files.has_key(filename):
475 file = self.files[filename]
476 request.setHeader("Content-Length", str(file.legacyftp.filesize))
477 request.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp.filename.encode("utf-8"))
478 return ""
479 else:
480 page = error.NoResource(message="404 File Not Found")
481 return page.render(request)
482
483