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