]> code.delx.au - pymsnt/blob - src/ft.py
SOCKS5 receiving 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 class FTReceive:
16 """ Manager for file transfers going from MSN to Jabber. """
17
18 """
19 Plan of action for this class:
20 * Determine the FT support of the Jabber client.
21 * If we find a common protocol, then send the invitation.
22 * Tell the legacyftp object the result of the invitation.
23 * If it was accepted, then start the transfer.
24
25 """
26
27 def __init__(self, session, senderJID, legacyftp):
28 self.session = session
29 self.toJID = self.session.jabberID + "/" + self.session.highestResource()
30 self.senderJID = senderJID
31 self.ident = (self.toJID, self.senderJID)
32 self.legacyftp = legacyftp
33 LogEvent(INFO)
34 self.checkSupport()
35
36 def checkSupport(self):
37 def discoDone(features):
38 LogEvent(INFO, self.ident)
39 enabledS5B = hasattr(self.session.pytrans, "ftSOCKS5")
40 enabledOOB = hasattr(self.session.pytrans, "ftOOB")
41 hasFT = features.count(disco.FT)
42 hasS5B = features.count(disco.S5B)
43 hasOOB = features.count(disco.IQOOB)
44 LogEvent(INFO, self.ident, "Choosing transfer mode.")
45 if hasFT > 0 and hasS5B > 0 and enabledS5B:
46 self.socksMode()
47 elif hasOOB > 0 and enabledOOB:
48 self.oobMode()
49 elif enabledOOB:
50 self.messageOobMode()
51 else:
52 # No support
53 self.legacyftp.reject()
54 del self.legacyftp
55
56 def discoFail(err=None):
57 LogEvent(INFO, self.ident, str(err))
58 self.messageOobMode()
59
60 d = disco.DiscoRequest(self.session.pytrans, self.toJID).doDisco()
61 d.addCallbacks(discoDone, discoFail)
62
63 def socksMode(self):
64 def ftReply(el):
65 if el.getAttribute("type") != "result":
66 ftDeclined()
67 return
68 self.session.pytrans.ftSOCKS5.addConnection(utils.socks5Hash(self.sid, self.senderJID, self.toJID), self.legacyftp)
69 LogEvent(INFO, self.ident)
70 iq = Element((None, "iq"))
71 iq.attributes["type"] = "set"
72 iq.attributes["to"] = self.toJID
73 iq.attributes["from"] = self.senderJID
74 query = iq.addElement("query")
75 query.attributes["xmlns"] = disco.S5B
76 query.attributes["sid"] = self.sid
77 query.attributes["mode"] = "tcp"
78 streamhost = query.addElement("streamhost")
79 streamhost.attributes["jid"] = self.senderJID
80 streamhost.attributes["host"] = config.ip
81 streamhost.attributes["port"] = config.ftJabberPort
82 d = self.session.pytrans.discovery.sendIq(iq)
83 d.addErrback(ftDeclined) # Timeout
84
85 def ftDeclined(el):
86 self.legacyftp.reject()
87 del self.legacyftp
88
89 LogEvent(INFO, self.ident)
90 self.sid = str(random.randint(1000, sys.maxint))
91 iq = Element((None, "iq"))
92 iq.attributes["type"] = "set"
93 iq.attributes["to"] = self.toJID
94 iq.attributes["from"] = self.senderJID
95 si = iq.addElement("si")
96 si.attributes["xmlns"] = disco.SI
97 si.attributes["profile"] = disco.FT
98 si.attributes["id"] = self.sid
99 file = si.addElement("file")
100 file.attributes["profile"] = disco.FT
101 file.attributes["size"] = str(self.legacyftp.filesize)
102 file.attributes["name"] = self.legacyftp.filename
103 # Feature negotiation
104 feature = si.addElement("feature")
105 feature.attributes["xmlns"] = disco.FEATURE_NEG
106 x = feature.addElement("x")
107 x.attributes["xmlns"] = "jabber:x:data"
108 x.attributes["type"] = "form"
109 field = x.addElement("field")
110 field.attributes["type"] = "list-single"
111 field.attributes["var"] = "stream-method"
112 option = field.addElement("option")
113 value = option.addElement("value")
114 value.addContent(disco.S5B)
115 d = self.session.pytrans.discovery.sendIq(iq, 60*3)
116 d.addCallback(ftReply)
117 d.addErrback(ftDeclined)
118
119 def oobMode(self):
120 def cb(el):
121 if el.getAttribute("type") != "result":
122 self.legacyftp.reject()
123 del self.legacyftp
124 self.session.pytrans.ftOOB.remFile(filename)
125
126 def ecb(ignored=None):
127 self.legacyftp.reject()
128 del self.legacyftp
129
130 LogEvent(INFO, self.ident)
131 filename = self.session.pytrans.ftOOB.putFile(self, self.legacyftp.filename)
132 iq = Element((None, "iq"))
133 iq.attributes["to"] = self.toJID
134 iq.attributes["from"] = self.senderJID
135 query = m.addElement("query")
136 query.attributes["xmlns"] = disco.IQOOB
137 query.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
138 d = self.session.send(iq)
139 d.addCallbacks(cb, ecb)
140
141 def messageOobMode(self):
142 LogEvent(INFO, self.ident)
143 filename = self.session.pytrans.ftOOB.putFile(self, self.legacyftp.filename)
144 m = Element((None, "message"))
145 m.attributes["to"] = self.session.jabberID
146 m.attributes["from"] = self.senderJID
147 m.addElement("body").addContent(config.ftOOBRoot + "/" + filename)
148 x = m.addElement("x")
149 x.attributes["xmlns"] = disco.XOOB
150 x.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
151 self.session.pytrans.send(m)
152
153 def error(self, ignored=None):
154 # FIXME
155 LogEvent(WARN)
156
157
158
159 # SOCKS5
160
161 from tlib import socks5
162
163 class JEP65Connection(socks5.SOCKSv5):
164 def __init__(self, listener):
165 socks5.SOCKSv5.__init__(self)
166 self.listener = listener
167 self.supportedAuthMechs = [socks5.AUTHMECH_ANON]
168 self.supportedAddrs = [socks5.ADDR_DOMAINNAME]
169 self.enabledCommands = [socks5.CMD_CONNECT]
170 self.addr = ""
171
172 def connectRequested(self, addr, port):
173 # So that the legacyftp can close the connection
174 self.transport.close = self.transport.loseConnection
175
176 # Check for special connect to the namespace -- this signifies that
177 # the client is just checking that it can connect to the streamhost
178 if addr == disco.S5B:
179 self.connectCompleted(addr, 0)
180 self.transport.loseConnection()
181 return
182
183 self.addr = addr
184
185 if self.listener.isActive(addr):
186 self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED)
187 return
188
189 if self.listener.addConnection(addr, self):
190 self.connectCompleted(addr, 0)
191 else:
192 self.sendErrorReply(socks5.REPLY_CONN_REFUSED)
193
194 def connectionLost(self, reason):
195 if self.state == socks5.STATE_CONNECT_PENDING:
196 self.listener.removePendingConnection(self.addr, self)
197 else:
198 self.transport.unregisterProducer()
199 if self.peersock != None:
200 self.peersock.peersock = None
201 self.peersock.transport.unregisterProducer()
202 self.peersock = None
203 self.listener.removeActiveConnection(self.addr)
204
205 class Proxy65(protocol.Factory):
206 def __init__(self, port):
207 LogEvent(INFO)
208 reactor.listenTCP(port, self)
209 self.pendingConns = {}
210 self.activeConns = {}
211
212 def buildProtocol(self, addr):
213 return JEP65Connection(self)
214
215 def isActive(self, address):
216 return address in self.activeConns
217
218 def activateStream(self, address):
219 if address in self.pendingConns:
220 olist = self.pendingConns[address]
221 if len(olist) != 2:
222 LogEvent(WARN, '', "Not exactly two!")
223 return
224
225 assert address not in self.activeConns
226 self.activeConns[address] = None
227
228 if not isinstance(olist[0], JEP65Connection):
229 legacyftp = olist[0]
230 connection = olist[1]
231 elif not isinstance(olist[1], JEP65Connection):
232 legacyftp = olist[1]
233 connection = olist[0]
234 else:
235 LogEvent(WARN, '', "No legacyftp")
236 return
237
238 legacyftp.accept(connection.transport)
239 else:
240 LogEvent(WARN, '', "No pending connection.")
241
242 def addConnection(self, address, connection):
243 olist = self.pendingConns.get(address, [])
244 if len(olist) <= 1:
245 olist.append(connection)
246 self.pendingConns[address] = olist
247 if len(olist) == 2:
248 self.activateStream(address)
249 return True
250 else:
251 return False
252
253 def removePendingConnection(self, address, connection):
254 olist = self.pendingConns[address]
255 if len(olist) == 1:
256 del self.pendingConns[address]
257 else:
258 olist.remove(connection)
259
260 def removeActiveConnection(self, address):
261 del self.activeConns[address]
262
263
264 # OOB download server
265
266 from twisted.web import server, resource, error
267 from twisted.internet import reactor
268
269 from debug import LogEvent, INFO, WARN, ERROR
270
271 class Connector:
272 def __init__(self, ftReceive, ftHttpPush):
273 self.ftReceive, self.ftHttpPush = ftReceive, ftHttpPush
274 self.ftReceive.legacyftp.accept(self)
275
276 def write(self, data):
277 self.ftHttpPush.write(data)
278
279 def close(self):
280 self.ftHttpPush.finish()
281
282 def error(self):
283 self.ftHttpPush.finish()
284 self.ftReceive.error()
285
286 class FileTransferOOB(resource.Resource):
287 def __init__(self, port):
288 LogEvent(INFO)
289 self.isLeaf = True
290 self.files = {}
291 self.oobSite = server.Site(self)
292 reactor.listenTCP(port, self.oobSite)
293
294 def putFile(self, file, filename):
295 path = str(random.randint(100000000, 999999999))
296 filename = (path + "/" + filename).replace("//", "/")
297 self.files[filename] = file
298 return filename
299
300 def remFile(self, filename):
301 if self.files.has_key(filename):
302 del self.files[filename]
303
304 def render_GET(self, request):
305 filename = request.path[1:] # Remove the leading /
306 if self.files.has_key(filename):
307 file = self.files[filename]
308 request.setHeader("Content-Length", str(file.legacyftp.filesize))
309 request.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp.filename.encode("utf-8"))
310 Connector(file, request)
311 del self.files[filename]
312 return server.NOT_DONE_YET
313 else:
314 page = error.NoResource(message="404 File Not Found")
315 return page.render(request)
316
317
318