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