]>
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
.throttle
import Throttler
5 from tlib
.xmlw
import Element
6 from twisted
.internet
import protocol
10 from debug
import LogEvent
, INFO
, WARN
, ERROR
18 def doRateLimit(setConsumer
, consumer
):
20 rateLimit
= int(config
.ftRateLimit
)
24 throttler
= Throttler(consumer
, rateLimit
)
25 setConsumer(throttler
)
29 def checkSizeOk(size
):
32 limit
= int(config
.ftSizeLimit
)
44 """ For file transfers going from Jabber to MSN. """
45 def __init__(self
, session
, to
, startTransfer
, cancelTransfer
, filename
, filesize
):
46 self
.startTransfer
= startTransfer
47 self
.cancelTransfer
= cancelTransfer
48 self
.filename
= filename
49 self
.filesize
= filesize
50 if not checkSizeOk(self
.filesize
):
51 LogEvent(INFO
, session
.jabberID
, "File too large.")
52 session
.legacycon
.sendMessage(to
, "", lang
.get(session
.lang
).msnFtSizeRejected
% (self
.filename
, config
.ftSizeLimit
, config
.website
), True)
56 session
.legacycon
.sendFile(to
, self
)
58 def accept(self
, legacyFileSend
):
59 doRateLimit(self
.startTransfer
, legacyFileSend
)
67 del self
.startTransfer
, self
.cancelTransfer
71 from twisted
.web
import http
74 from twisted
.protocols
import http
76 print "Couldn't find http.HTTPClient. If you're using Twisted 2.0, make sure that you've installed twisted.web"
80 class OOBHeaderHelper(http
.HTTPClient
):
81 """ Makes a HEAD request and grabs the length """
82 def connectionMade(self
):
83 self
.sendCommand("HEAD", self
.factory
.path
.encode("utf-8"))
84 self
.sendHeader("Host", (self
.factory
.host
+ ":" + str(self
.factory
.port
)).encode("utf-8"))
87 def handleEndHeaders(self
):
88 self
.factory
.gotLength(self
.length
)
90 def handleResponse(self
, data
):
94 class OOBSendConnector(http
.HTTPClient
):
95 def connectionMade(self
):
96 self
.sendCommand("GET", self
.factory
.path
.encode("utf-8"))
97 self
.sendHeader("Host", (self
.factory
.host
+ ":" + str(self
.factory
.port
)).encode("utf-8"))
101 def handleResponsePart(self
, data
):
102 self
.factory
.consumer
.write(data
)
104 def handleResponseEnd(self
):
105 # This is called once before writing is finished, and once when the
106 # connection closes. We only consumer.close() on the second.
110 self
.factory
.consumer
.close()
111 self
.factory
.consumer
= None
112 self
.factory
.finished()
123 """ For file transfers going from MSN to Jabber. """
126 Plan of action for this class:
127 * Determine the FT support of the Jabber client.
128 * If we find a common protocol, then send the invitation.
129 * Tell the legacyftp object the result of the invitation.
130 * If it was accepted, then start the transfer.
134 def __init__(self
, session
, senderJID
, legacyftp
):
135 if not checkSizeOk(legacyftp
.filesize
):
136 LogEvent(INFO
, session
.jabberID
, "File too large.")
138 session
.legacycon
.sendMessage(senderJID
, "", lang
.get(session
.lang
).msnFtSizeRejected
% (legacyftp
.filename
, config
.ftSizeLimit
, config
.website
), False)
140 self
.session
= session
141 self
.toJID
= self
.session
.jabberID
+ "/" + self
.session
.highestResource()
142 self
.senderJID
= senderJID
143 self
.ident
= (self
.toJID
, self
.senderJID
)
144 self
.legacyftp
= legacyftp
145 LogEvent(INFO
, session
.jabberID
)
148 def checkSupport(self
):
149 def discoDone(features
):
150 LogEvent(INFO
, self
.ident
)
151 enabledS5B
= hasattr(self
.session
.pytrans
, "ftSOCKS5Receive")
152 enabledOOB
= hasattr(self
.session
.pytrans
, "ftOOBReceive")
153 hasFT
= features
.count(disco
.FT
)
154 hasS5B
= features
.count(disco
.S5B
)
155 hasOOB
= features
.count(disco
.IQOOB
)
156 LogEvent(INFO
, self
.ident
, "Choosing transfer mode.")
157 if hasFT
> 0 and hasS5B
> 0 and enabledS5B
:
159 elif hasOOB
> 0 and enabledOOB
:
162 self
.messageOobMode()
165 self
.legacyftp
.reject()
168 def discoFail(err
=None):
169 LogEvent(INFO
, self
.ident
, str(err
))
170 if hasattr(self
.session
.pytrans
, "ftOOBReceive"):
171 self
.messageOobMode()
174 self
.legacyftp
.reject()
177 d
= disco
.DiscoRequest(self
.session
.pytrans
, self
.toJID
).doDisco()
178 d
.addCallbacks(discoDone
, discoFail
)
182 if el
.getAttribute("type") != "result":
185 self
.session
.pytrans
.ftSOCKS5Receive
.addConnection(utils
.socks5Hash(self
.sid
, self
.senderJID
, self
.toJID
), self
.legacyftp
)
186 LogEvent(INFO
, self
.ident
)
187 iq
= Element((None, "iq"))
188 iq
.attributes
["type"] = "set"
189 iq
.attributes
["to"] = self
.toJID
190 iq
.attributes
["from"] = self
.senderJID
191 query
= iq
.addElement("query")
192 query
.attributes
["xmlns"] = disco
.S5B
193 query
.attributes
["sid"] = self
.sid
194 query
.attributes
["mode"] = "tcp"
195 streamhost
= query
.addElement("streamhost")
196 streamhost
.attributes
["jid"] = self
.senderJID
197 streamhost
.attributes
["host"] = config
.host
198 streamhost
.attributes
["port"] = config
.ftJabberPort
199 d
= self
.session
.pytrans
.discovery
.sendIq(iq
)
200 d
.addErrback(ftDeclined
) # Timeout
203 self
.legacyftp
.reject()
206 LogEvent(INFO
, self
.ident
)
207 self
.sid
= str(random
.randint(1000, sys
.maxint
))
208 iq
= Element((None, "iq"))
209 iq
.attributes
["type"] = "set"
210 iq
.attributes
["to"] = self
.toJID
211 iq
.attributes
["from"] = self
.senderJID
212 si
= iq
.addElement("si")
213 si
.attributes
["xmlns"] = disco
.SI
214 si
.attributes
["profile"] = disco
.FT
215 si
.attributes
["id"] = self
.sid
216 file = si
.addElement("file")
217 file.attributes
["xmlns"] = disco
.FT
218 file.attributes
["size"] = str(self
.legacyftp
.filesize
)
219 file.attributes
["name"] = self
.legacyftp
.filename
220 # Feature negotiation
221 feature
= si
.addElement("feature")
222 feature
.attributes
["xmlns"] = disco
.FEATURE_NEG
223 x
= feature
.addElement("x")
224 x
.attributes
["xmlns"] = disco
.XDATA
225 x
.attributes
["type"] = "form"
226 field
= x
.addElement("field")
227 field
.attributes
["type"] = "list-single"
228 field
.attributes
["var"] = "stream-method"
229 option
= field
.addElement("option")
230 value
= option
.addElement("value")
231 value
.addContent(disco
.S5B
)
232 d
= self
.session
.pytrans
.discovery
.sendIq(iq
, 60*3)
233 d
.addCallback(ftReply
)
234 d
.addErrback(ftDeclined
)
238 if el
.getAttribute("type") != "result":
239 self
.legacyftp
.reject()
241 self
.session
.pytrans
.ftOOBReceive
.remFile(filename
)
243 def ecb(ignored
=None):
244 self
.legacyftp
.reject()
247 LogEvent(INFO
, self
.ident
)
248 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
249 iq
= Element((None, "iq"))
250 iq
.attributes
["to"] = self
.toJID
251 iq
.attributes
["from"] = self
.senderJID
252 query
= m
.addElement("query")
253 query
.attributes
["xmlns"] = disco
.IQOOB
254 query
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
255 d
= self
.session
.send(iq
)
256 d
.addCallbacks(cb
, ecb
)
258 def messageOobMode(self
):
259 LogEvent(INFO
, self
.ident
)
260 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
261 m
= Element((None, "message"))
262 m
.attributes
["to"] = self
.session
.jabberID
263 m
.attributes
["from"] = self
.senderJID
264 m
.addElement("body").addContent(config
.ftOOBRoot
+ "/" + filename
)
265 x
= m
.addElement("x")
266 x
.attributes
["xmlns"] = disco
.XOOB
267 x
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
268 self
.session
.pytrans
.send(m
)
270 def error(self
, ignored
=None):
278 from tlib
import socks5
281 class JEP65ConnectionSend(protocol
.Protocol
):
282 # TODO, clean up and move this to tlib.socks5
284 STATE_WAIT_AUTHOK
= 2
285 STATE_WAIT_CONNECTOK
= 3
289 self
.state
= self
.STATE_INITIAL
292 def connectionMade(self
):
293 self
.transport
.write(struct
.pack("!BBB", 5, 1, 0))
294 self
.state
= self
.STATE_WAIT_AUTHOK
296 def connectionLost(self
, reason
):
297 if self
.state
== self
.STATE_READY
:
298 self
.factory
.consumer
.close()
300 def _waitAuthOk(self
):
301 ver
, method
= struct
.unpack("!BB", self
.buf
[:2])
302 if ver
!= 5 or method
!= 0:
303 self
.transport
.loseConnection()
305 self
.buf
= self
.buf
[2:] # chop
307 # Send CONNECT request
308 length
= len(self
.factory
.hash)
309 self
.transport
.write(struct
.pack("!BBBBB", 5, 1, 0, 3, length
))
310 self
.transport
.write("".join([struct
.pack("!B" , ord(x
))[0] for x
in self
.factory
.hash]))
311 self
.transport
.write(struct
.pack("!H", 0))
312 self
.state
= self
.STATE_WAIT_CONNECTOK
314 def _waitConnectOk(self
):
315 ver
, rep
, rsv
, atyp
= struct
.unpack("!BBBB", self
.buf
[:4])
316 if not (ver
== 5 and rep
== 0):
317 self
.transport
.loseConnection()
320 self
.state
= self
.STATE_READY
321 self
.factory
.madeConnection(self
.transport
.addr
[0])
323 def dataReceived(self
, buf
):
324 if self
.state
== self
.STATE_READY
:
325 self
.factory
.consumer
.write(buf
)
328 if self
.state
== self
.STATE_WAIT_AUTHOK
:
330 elif self
.state
== self
.STATE_WAIT_CONNECTOK
:
331 self
._waitConnectOk
()
334 class JEP65ConnectionReceive(socks5
.SOCKSv5
):
335 def __init__(self
, listener
):
336 socks5
.SOCKSv5
.__init__(self
)
337 self
.listener
= listener
338 self
.supportedAuthMechs
= [socks5
.AUTHMECH_ANON
]
339 self
.supportedAddrs
= [socks5
.ADDR_DOMAINNAME
]
340 self
.enabledCommands
= [socks5
.CMD_CONNECT
]
343 def connectRequested(self
, addr
, port
):
344 # So that the legacyftp can close the connection
345 self
.transport
.close
= self
.transport
.loseConnection
347 # Check for special connect to the namespace -- this signifies that
348 # the client is just checking that it can connect to the streamhost
349 if addr
== disco
.S5B
:
350 self
.connectCompleted(addr
, 0)
351 self
.transport
.loseConnection()
356 if self
.listener
.isActive(addr
):
357 self
.sendErrorReply(socks5
.REPLY_CONN_NOT_ALLOWED
)
360 if self
.listener
.addConnection(addr
, self
):
361 self
.connectCompleted(addr
, 0)
363 self
.sendErrorReply(socks5
.REPLY_CONN_REFUSED
)
365 def connectionLost(self
, reason
):
366 if self
.state
== socks5
.STATE_CONNECT_PENDING
:
367 self
.listener
.removePendingConnection(self
.addr
, self
)
369 self
.transport
.unregisterProducer()
370 if self
.peersock
!= None:
371 self
.peersock
.peersock
= None
372 self
.peersock
.transport
.unregisterProducer()
374 self
.listener
.removeActiveConnection(self
.addr
)
376 class Proxy65(protocol
.Factory
):
377 def __init__(self
, port
):
379 reactor
.listenTCP(port
, self
)
380 self
.pendingConns
= {}
381 self
.activeConns
= {}
383 def buildProtocol(self
, addr
):
384 return JEP65ConnectionReceive(self
)
386 def isActive(self
, address
):
387 return address
in self
.activeConns
389 def activateStream(self
, address
):
390 if address
in self
.pendingConns
:
391 olist
= self
.pendingConns
[address
]
393 LogEvent(WARN
, '', "Not exactly two!")
396 assert address
not in self
.activeConns
397 self
.activeConns
[address
] = None
399 if not isinstance(olist
[0], JEP65ConnectionReceive
):
401 connection
= olist
[1]
402 elif not isinstance(olist
[1], JEP65ConnectionReceive
):
404 connection
= olist
[0]
406 LogEvent(WARN
, '', "No JEP65Connection")
409 doRateLimit(legacyftp
.accept
, connection
.transport
)
411 LogEvent(WARN
, '', "No pending connection.")
413 def addConnection(self
, address
, connection
):
414 olist
= self
.pendingConns
.get(address
, [])
416 olist
.append(connection
)
417 self
.pendingConns
[address
] = olist
419 self
.activateStream(address
)
424 def removePendingConnection(self
, address
, connection
):
425 olist
= self
.pendingConns
[address
]
427 del self
.pendingConns
[address
]
429 olist
.remove(connection
)
431 def removeActiveConnection(self
, address
):
432 del self
.activeConns
[address
]
435 # OOB download server
437 from twisted
.web
import server
, resource
, error
438 from twisted
.internet
import reactor
440 from debug
import LogEvent
, INFO
, WARN
, ERROR
442 class OOBReceiveConnector
:
443 def __init__(self
, ftReceive
, ftHttpPush
):
444 self
.ftReceive
, self
.ftHttpPush
= ftReceive
, ftHttpPush
445 doRateLimit(self
.ftReceive
.legacyftp
.accept
, self
)
447 def write(self
, data
):
448 self
.ftHttpPush
.write(data
)
451 self
.ftHttpPush
.finish()
454 self
.ftHttpPush
.finish()
455 self
.ftReceive
.error()
457 class FileTransferOOBReceive(resource
.Resource
):
458 def __init__(self
, port
):
462 self
.oobSite
= server
.Site(self
)
463 reactor
.listenTCP(port
, self
.oobSite
)
465 def putFile(self
, file, filename
):
466 path
= str(random
.randint(100000000, 999999999))
467 filename
= (path
+ "/" + filename
).replace("//", "/")
468 self
.files
[filename
] = file
471 def remFile(self
, filename
):
472 if self
.files
.has_key(filename
):
473 del self
.files
[filename
]
475 def render_GET(self
, request
):
476 filename
= request
.path
[1:] # Remove the leading /
477 if self
.files
.has_key(filename
):
478 file = self
.files
[filename
]
479 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
480 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
481 OOBReceiveConnector(file, request
)
482 del self
.files
[filename
]
483 return server
.NOT_DONE_YET
485 page
= error
.NoResource(message
="404 File Not Found")
486 return page
.render(request
)
488 def render_HEAD(self
, request
):
489 filename
= request
.path
[1:] # Remove the leading /
490 if self
.files
.has_key(filename
):
491 file = self
.files
[filename
]
492 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
493 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
496 page
= error
.NoResource(message
="404 File Not Found")
497 return page
.render(request
)