]>
code.delx.au - pymsnt/blob - 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
4 from tlib
.xmlw
import Element
5 from twisted
.internet
import protocol
8 from debug
import LogEvent
, INFO
, WARN
, ERROR
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
28 def accept(self
, legacyFileSend
):
29 self
.startTransfer(legacyFileSend
)
32 del self
.startTransfer
37 from twisted
.web
import http
40 from twisted
.protocols
import http
42 print "Couldn't find http.HTTPClient. If you're using Twisted 2.0, make sure that you've installed twisted.web"
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"))
53 def handleEndHeaders(self
):
54 self
.factory
.gotLength(self
.length
)
56 def handleResponse(self
, data
):
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"))
67 def handleResponsePart(self
, data
):
68 self
.factory
.consumer
.write(data
)
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.
76 self
.factory
.consumer
.close()
77 self
.factory
.consumer
= None
78 self
.factory
.finished()
89 """ For file transfers going from MSN to Jabber. """
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.
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
109 def checkSupport(self
):
110 def discoDone(features
):
111 LogEvent(INFO
, self
.ident
)
112 enabledS5B
= hasattr(self
.session
.pytrans
, "ftSOCKS5Receive")
113 enabledOOB
= hasattr(self
.session
.pytrans
, "ftOOBReceive")
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
:
120 elif hasOOB
> 0 and enabledOOB
:
123 self
.messageOobMode()
126 self
.legacyftp
.reject()
129 def discoFail(err
=None):
130 LogEvent(INFO
, self
.ident
, str(err
))
131 self
.messageOobMode()
133 d
= disco
.DiscoRequest(self
.session
.pytrans
, self
.toJID
).doDisco()
134 d
.addCallbacks(discoDone
, discoFail
)
138 if el
.getAttribute("type") != "result":
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
159 self
.legacyftp
.reject()
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"] = disco
.XDATA
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
)
194 if el
.getAttribute("type") != "result":
195 self
.legacyftp
.reject()
197 self
.session
.pytrans
.ftOOB
.remFile(filename
)
199 def ecb(ignored
=None):
200 self
.legacyftp
.reject()
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
)
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
)
226 def error(self
, ignored
=None):
234 from tlib
import socks5
237 class JEP65ConnectionSend(protocol
.Protocol
):
238 # TODO, clean up and move this to tlib.socks5
240 STATE_WAIT_AUTHOK
= 2
241 STATE_WAIT_CONNECTOK
= 3
245 self
.state
= self
.STATE_INITIAL
248 def connectionMade(self
):
249 self
.transport
.write(struct
.pack("!BBB", 5, 1, 0))
250 self
.state
= self
.STATE_WAIT_AUTHOK
252 def connectionLost(self
, reason
):
253 if self
.state
== self
.STATE_READY
:
254 self
.factory
.consumer
.close()
256 def _waitAuthOk(self
):
257 ver
, method
= struct
.unpack("!BB", self
.buf
[:2])
258 if ver
!= 5 or method
!= 0:
259 self
.transport
.loseConnection()
261 self
.buf
= self
.buf
[2:] # chop
263 # Send CONNECT request
264 length
= len(self
.factory
.hash)
265 self
.transport
.write(struct
.pack("!BBBBB", 5, 1, 0, 3, length
))
266 self
.transport
.write("".join([struct
.pack("!B" , ord(x
))[0] for x
in self
.factory
.hash]))
267 self
.transport
.write(struct
.pack("!H", 0))
268 self
.state
= self
.STATE_WAIT_CONNECTOK
270 def _waitConnectOk(self
):
271 ver
, rep
, rsv
, atyp
= struct
.unpack("!BBBB", self
.buf
[:4])
272 if not (ver
== 5 and rep
== 0):
273 self
.transport
.loseConnection()
276 self
.state
= self
.STATE_READY
277 self
.factory
.madeConnection(self
.transport
.addr
[0])
279 def dataReceived(self
, buf
):
280 if self
.state
== self
.STATE_READY
:
281 self
.factory
.consumer
.write(buf
)
284 if self
.state
== self
.STATE_WAIT_AUTHOK
:
286 elif self
.state
== self
.STATE_WAIT_CONNECTOK
:
287 self
._waitConnectOk
()
290 class JEP65ConnectionReceive(socks5
.SOCKSv5
):
291 def __init__(self
, listener
):
292 socks5
.SOCKSv5
.__init__(self
)
293 self
.listener
= listener
294 self
.supportedAuthMechs
= [socks5
.AUTHMECH_ANON
]
295 self
.supportedAddrs
= [socks5
.ADDR_DOMAINNAME
]
296 self
.enabledCommands
= [socks5
.CMD_CONNECT
]
299 def connectRequested(self
, addr
, port
):
300 # So that the legacyftp can close the connection
301 self
.transport
.close
= self
.transport
.loseConnection
303 # Check for special connect to the namespace -- this signifies that
304 # the client is just checking that it can connect to the streamhost
305 if addr
== disco
.S5B
:
306 self
.connectCompleted(addr
, 0)
307 self
.transport
.loseConnection()
312 if self
.listener
.isActive(addr
):
313 self
.sendErrorReply(socks5
.REPLY_CONN_NOT_ALLOWED
)
316 if self
.listener
.addConnection(addr
, self
):
317 self
.connectCompleted(addr
, 0)
319 self
.sendErrorReply(socks5
.REPLY_CONN_REFUSED
)
321 def connectionLost(self
, reason
):
322 if self
.state
== socks5
.STATE_CONNECT_PENDING
:
323 self
.listener
.removePendingConnection(self
.addr
, self
)
325 self
.transport
.unregisterProducer()
326 if self
.peersock
!= None:
327 self
.peersock
.peersock
= None
328 self
.peersock
.transport
.unregisterProducer()
330 self
.listener
.removeActiveConnection(self
.addr
)
332 class Proxy65(protocol
.Factory
):
333 def __init__(self
, port
):
335 reactor
.listenTCP(port
, self
)
336 self
.pendingConns
= {}
337 self
.activeConns
= {}
339 def buildProtocol(self
, addr
):
340 return JEP65ConnectionReceive(self
)
342 def isActive(self
, address
):
343 return address
in self
.activeConns
345 def activateStream(self
, address
):
346 if address
in self
.pendingConns
:
347 olist
= self
.pendingConns
[address
]
349 LogEvent(WARN
, '', "Not exactly two!")
352 assert address
not in self
.activeConns
353 self
.activeConns
[address
] = None
355 if not isinstance(olist
[0], (JEP65ConnectionReceive
, JEP65ConnectionSend
)):
357 connection
= olist
[1]
358 elif not isinstance(olist
[1], (JEP65ConnectionReceive
, JEP65ConnectionSend
)):
360 connection
= olist
[0]
362 LogEvent(WARN
, '', "No JEP65Connection")
365 legacyftp
.accept(connection
.transport
)
367 LogEvent(WARN
, '', "No pending connection.")
369 def addConnection(self
, address
, connection
):
370 olist
= self
.pendingConns
.get(address
, [])
372 olist
.append(connection
)
373 self
.pendingConns
[address
] = olist
375 self
.activateStream(address
)
380 def removePendingConnection(self
, address
, connection
):
381 olist
= self
.pendingConns
[address
]
383 del self
.pendingConns
[address
]
385 olist
.remove(connection
)
387 def removeActiveConnection(self
, address
):
388 del self
.activeConns
[address
]
391 # OOB download server
393 from twisted
.web
import server
, resource
, error
394 from twisted
.internet
import reactor
396 from debug
import LogEvent
, INFO
, WARN
, ERROR
398 class OOBReceiveConnector
:
399 def __init__(self
, ftReceive
, ftHttpPush
):
400 self
.ftReceive
, self
.ftHttpPush
= ftReceive
, ftHttpPush
401 self
.ftReceive
.legacyftp
.accept(self
)
403 def write(self
, data
):
404 self
.ftHttpPush
.write(data
)
407 self
.ftHttpPush
.finish()
410 self
.ftHttpPush
.finish()
411 self
.ftReceive
.error()
413 class FileTransferOOBReceive(resource
.Resource
):
414 def __init__(self
, port
):
418 self
.oobSite
= server
.Site(self
)
419 reactor
.listenTCP(port
, self
.oobSite
)
421 def putFile(self
, file, filename
):
422 path
= str(random
.randint(100000000, 999999999))
423 filename
= (path
+ "/" + filename
).replace("//", "/")
424 self
.files
[filename
] = file
427 def remFile(self
, filename
):
428 if self
.files
.has_key(filename
):
429 del self
.files
[filename
]
431 def render_GET(self
, request
):
432 filename
= request
.path
[1:] # Remove the leading /
433 if self
.files
.has_key(filename
):
434 file = self
.files
[filename
]
435 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
436 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
437 OOBReceiveConnector(file, request
)
438 del self
.files
[filename
]
439 return server
.NOT_DONE_YET
441 page
= error
.NoResource(message
="404 File Not Found")
442 return page
.render(request
)
444 def render_HEAD(self
, request
):
445 filename
= request
.path
[1:] # Remove the leading /
446 if self
.files
.has_key(filename
):
447 file = self
.files
[filename
]
448 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
449 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
452 page
= error
.NoResource(message
="404 File Not Found")
453 return page
.render(request
)