]>
code.delx.au - pymsnt/blob - src/ft.py
1 # Copyright 2005-2006 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
4 from throttle
import Throttler
5 from twisted
.internet
import protocol
6 from twisted
.words
.xish
.domish
import Element
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 text
= lang
.get(session
.lang
).msnFtSizeRejected
% (self
.filename
, config
.ftSizeLimit
, config
.website
)
53 session
.legacycon
.sendMessage(to
, "", text
, True)
54 session
.sendMessage(to
=session
.jabberID
, fro
=to
, body
=text
)
58 session
.legacycon
.sendFile(to
, self
)
60 def accept(self
, legacyFileSend
):
61 doRateLimit(self
.startTransfer
, legacyFileSend
)
69 del self
.startTransfer
, self
.cancelTransfer
73 from twisted
.web
import http
76 from twisted
.protocols
import http
78 print "Couldn't find http.HTTPClient. If you're using Twisted 2.0, make sure that you've installed twisted.web"
82 class OOBHeaderHelper(http
.HTTPClient
):
83 """ Makes a HEAD request and grabs the length """
84 def connectionMade(self
):
85 self
.sendCommand("HEAD", self
.factory
.path
.encode("utf-8"))
86 self
.sendHeader("Host", (self
.factory
.host
+ ":" + str(self
.factory
.port
)).encode("utf-8"))
89 def handleEndHeaders(self
):
90 self
.factory
.gotLength(self
.length
)
92 def handleResponse(self
, data
):
96 class OOBSendConnector(http
.HTTPClient
):
97 def connectionMade(self
):
98 self
.sendCommand("GET", self
.factory
.path
.encode("utf-8"))
99 self
.sendHeader("Host", (self
.factory
.host
+ ":" + str(self
.factory
.port
)).encode("utf-8"))
103 def handleResponsePart(self
, data
):
104 self
.factory
.consumer
.write(data
)
106 def handleResponseEnd(self
):
107 # This is called once before writing is finished, and once when the
108 # connection closes. We only consumer.close() on the second.
112 self
.factory
.consumer
.close()
113 self
.factory
.consumer
= None
114 self
.factory
.finished()
125 """ For file transfers going from MSN to Jabber. """
128 Plan of action for this class:
129 * Determine the FT support of the Jabber client.
130 * If we find a common protocol, then send the invitation.
131 * Tell the legacyftp object the result of the invitation.
132 * If it was accepted, then start the transfer.
136 def __init__(self
, session
, senderJID
, legacyftp
):
137 if not checkSizeOk(legacyftp
.filesize
):
138 LogEvent(INFO
, session
.jabberID
, "File too large.")
140 text
= lang
.get(session
.lang
).msnFtSizeRejected
% (legacyftp
.filename
, config
.ftSizeLimit
, config
.website
)
141 session
.legacycon
.sendMessage(senderJID
, "", text
, False)
142 session
.sendMessage(to
=session
.jabberID
, fro
=senderJID
, body
=text
)
144 self
.session
= session
145 self
.toJID
= self
.session
.jabberID
+ "/" + self
.session
.highestResource()
146 self
.senderJID
= senderJID
147 self
.ident
= (self
.toJID
, self
.senderJID
)
148 self
.legacyftp
= legacyftp
149 LogEvent(INFO
, session
.jabberID
)
152 def checkSupport(self
):
153 def discoDone(features
):
154 LogEvent(INFO
, self
.ident
)
155 enabledS5B
= hasattr(self
.session
.pytrans
, "ftSOCKS5Receive")
156 enabledOOB
= hasattr(self
.session
.pytrans
, "ftOOBReceive")
157 hasFT
= features
.count(disco
.FT
)
158 hasS5B
= features
.count(disco
.S5B
)
159 hasOOB
= features
.count(disco
.IQOOB
)
160 LogEvent(INFO
, self
.ident
, "Choosing transfer mode.")
161 if hasFT
> 0 and hasS5B
> 0 and enabledS5B
:
163 elif hasOOB
> 0 and enabledOOB
:
166 self
.messageOobMode()
169 self
.legacyftp
.reject()
172 def discoFail(err
=None):
173 LogEvent(INFO
, self
.ident
, str(err
))
174 if hasattr(self
.session
.pytrans
, "ftOOBReceive"):
175 self
.messageOobMode()
178 self
.legacyftp
.reject()
181 d
= disco
.DiscoRequest(self
.session
.pytrans
, self
.toJID
).doDisco()
182 d
.addCallbacks(discoDone
, discoFail
)
186 if el
.getAttribute("type") != "result":
189 self
.session
.pytrans
.ftSOCKS5Receive
.addConnection(utils
.socks5Hash(self
.sid
, self
.senderJID
, self
.toJID
), self
.legacyftp
)
190 LogEvent(INFO
, self
.ident
)
191 iq
= Element((None, "iq"))
192 iq
.attributes
["type"] = "set"
193 iq
.attributes
["to"] = self
.toJID
194 iq
.attributes
["from"] = self
.senderJID
195 query
= iq
.addElement("query")
196 query
.attributes
["xmlns"] = disco
.S5B
197 query
.attributes
["sid"] = self
.sid
198 query
.attributes
["mode"] = "tcp"
199 streamhost
= query
.addElement("streamhost")
200 streamhost
.attributes
["jid"] = self
.senderJID
201 streamhost
.attributes
["host"] = config
.host
202 streamhost
.attributes
["port"] = config
.ftJabberPort
203 d
= self
.session
.pytrans
.discovery
.sendIq(iq
)
204 d
.addErrback(ftDeclined
) # Timeout
207 self
.legacyftp
.reject()
210 LogEvent(INFO
, self
.ident
)
211 self
.sid
= str(random
.randint(1000, sys
.maxint
))
212 iq
= Element((None, "iq"))
213 iq
.attributes
["type"] = "set"
214 iq
.attributes
["to"] = self
.toJID
215 iq
.attributes
["from"] = self
.senderJID
216 si
= iq
.addElement("si")
217 si
.attributes
["xmlns"] = disco
.SI
218 si
.attributes
["profile"] = disco
.FT
219 si
.attributes
["id"] = self
.sid
220 file = si
.addElement("file")
221 file.attributes
["xmlns"] = disco
.FT
222 file.attributes
["size"] = str(self
.legacyftp
.filesize
)
223 file.attributes
["name"] = self
.legacyftp
.filename
224 # Feature negotiation
225 feature
= si
.addElement("feature")
226 feature
.attributes
["xmlns"] = disco
.FEATURE_NEG
227 x
= feature
.addElement("x")
228 x
.attributes
["xmlns"] = disco
.XDATA
229 x
.attributes
["type"] = "form"
230 field
= x
.addElement("field")
231 field
.attributes
["type"] = "list-single"
232 field
.attributes
["var"] = "stream-method"
233 option
= field
.addElement("option")
234 value
= option
.addElement("value")
235 value
.addContent(disco
.S5B
)
236 d
= self
.session
.pytrans
.discovery
.sendIq(iq
, 60*3)
237 d
.addCallback(ftReply
)
238 d
.addErrback(ftDeclined
)
242 if el
.getAttribute("type") != "result":
243 self
.legacyftp
.reject()
245 self
.session
.pytrans
.ftOOBReceive
.remFile(filename
)
247 def ecb(ignored
=None):
248 self
.legacyftp
.reject()
251 LogEvent(INFO
, self
.ident
)
252 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
253 iq
= Element((None, "iq"))
254 iq
.attributes
["to"] = self
.toJID
255 iq
.attributes
["from"] = self
.senderJID
256 query
= m
.addElement("query")
257 query
.attributes
["xmlns"] = disco
.IQOOB
258 query
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
259 d
= self
.session
.send(iq
)
260 d
.addCallbacks(cb
, ecb
)
262 def messageOobMode(self
):
263 LogEvent(INFO
, self
.ident
)
264 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
265 m
= Element((None, "message"))
266 m
.attributes
["to"] = self
.session
.jabberID
267 m
.attributes
["from"] = self
.senderJID
268 m
.addElement("body").addContent(config
.ftOOBRoot
+ "/" + filename
)
269 x
= m
.addElement("x")
270 x
.attributes
["xmlns"] = disco
.XOOB
271 x
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
272 self
.session
.pytrans
.send(m
)
274 def error(self
, ignored
=None):
285 class JEP65ConnectionSend(protocol
.Protocol
):
286 # TODO, clean up and move this to socks5
288 STATE_WAIT_AUTHOK
= 2
289 STATE_WAIT_CONNECTOK
= 3
293 self
.state
= self
.STATE_INITIAL
296 def connectionMade(self
):
297 self
.transport
.write(struct
.pack("!BBB", 5, 1, 0))
298 self
.state
= self
.STATE_WAIT_AUTHOK
300 def connectionLost(self
, reason
):
301 if self
.state
== self
.STATE_READY
:
302 self
.factory
.consumer
.close()
304 self
.factory
.consumer
.error()
306 def _waitAuthOk(self
):
307 ver
, method
= struct
.unpack("!BB", self
.buf
[:2])
308 if ver
!= 5 or method
!= 0:
309 self
.transport
.loseConnection()
311 self
.buf
= self
.buf
[2:] # chop
313 # Send CONNECT request
314 length
= len(self
.factory
.hash)
315 self
.transport
.write(struct
.pack("!BBBBB", 5, 1, 0, 3, length
))
316 self
.transport
.write("".join([struct
.pack("!B" , ord(x
))[0] for x
in self
.factory
.hash]))
317 self
.transport
.write(struct
.pack("!H", 0))
318 self
.state
= self
.STATE_WAIT_CONNECTOK
320 def _waitConnectOk(self
):
321 ver
, rep
, rsv
, atyp
= struct
.unpack("!BBBB", self
.buf
[:4])
322 if not (ver
== 5 and rep
== 0):
323 self
.transport
.loseConnection()
326 self
.state
= self
.STATE_READY
327 self
.factory
.madeConnection(self
.transport
.addr
[0])
329 def dataReceived(self
, buf
):
330 if self
.state
== self
.STATE_READY
:
331 self
.factory
.consumer
.write(buf
)
334 if self
.state
== self
.STATE_WAIT_AUTHOK
:
336 elif self
.state
== self
.STATE_WAIT_CONNECTOK
:
337 self
._waitConnectOk
()
340 class JEP65ConnectionReceive(socks5
.SOCKSv5
):
341 def __init__(self
, listener
):
342 socks5
.SOCKSv5
.__init__(self
)
343 self
.listener
= listener
344 self
.supportedAuthMechs
= [socks5
.AUTHMECH_ANON
]
345 self
.supportedAddrs
= [socks5
.ADDR_DOMAINNAME
]
346 self
.enabledCommands
= [socks5
.CMD_CONNECT
]
349 def connectRequested(self
, addr
, port
):
350 # So that the legacyftp can close the connection
351 self
.transport
.close
= self
.transport
.loseConnection
353 # Check for special connect to the namespace -- this signifies that
354 # the client is just checking that it can connect to the streamhost
355 if addr
== disco
.S5B
:
356 self
.connectCompleted(addr
, 0)
357 self
.transport
.loseConnection()
362 if self
.listener
.isActive(addr
):
363 self
.sendErrorReply(socks5
.REPLY_CONN_NOT_ALLOWED
)
366 if self
.listener
.addConnection(addr
, self
):
367 self
.connectCompleted(addr
, 0)
369 self
.sendErrorReply(socks5
.REPLY_CONN_REFUSED
)
371 def connectionLost(self
, reason
):
372 if self
.state
== socks5
.STATE_CONNECT_PENDING
:
373 self
.listener
.removePendingConnection(self
.addr
, self
)
375 self
.transport
.unregisterProducer()
376 if self
.peersock
!= None:
377 self
.peersock
.peersock
= None
378 self
.peersock
.transport
.unregisterProducer()
380 self
.listener
.removeActiveConnection(self
.addr
)
382 class Proxy65(protocol
.Factory
):
383 def __init__(self
, port
):
385 reactor
.listenTCP(port
, self
)
386 self
.pendingConns
= {}
387 self
.activeConns
= {}
389 def buildProtocol(self
, addr
):
390 return JEP65ConnectionReceive(self
)
392 def isActive(self
, address
):
393 return address
in self
.activeConns
395 def activateStream(self
, address
):
396 if address
in self
.pendingConns
:
397 olist
= self
.pendingConns
[address
]
399 LogEvent(WARN
, '', "Not exactly two!")
402 assert address
not in self
.activeConns
403 self
.activeConns
[address
] = None
405 if not isinstance(olist
[0], JEP65ConnectionReceive
):
407 connection
= olist
[1]
408 elif not isinstance(olist
[1], JEP65ConnectionReceive
):
410 connection
= olist
[0]
412 LogEvent(WARN
, '', "No JEP65Connection")
415 doRateLimit(legacyftp
.accept
, connection
.transport
)
417 LogEvent(WARN
, '', "No pending connection.")
419 def addConnection(self
, address
, connection
):
420 olist
= self
.pendingConns
.get(address
, [])
422 olist
.append(connection
)
423 self
.pendingConns
[address
] = olist
425 self
.activateStream(address
)
430 def removePendingConnection(self
, address
, connection
):
431 olist
= self
.pendingConns
[address
]
433 del self
.pendingConns
[address
]
435 olist
.remove(connection
)
437 def removeActiveConnection(self
, address
):
438 del self
.activeConns
[address
]
441 # OOB download server
443 from twisted
.web
import server
, resource
, error
444 from twisted
.internet
import reactor
446 from debug
import LogEvent
, INFO
, WARN
, ERROR
448 class OOBReceiveConnector
:
449 def __init__(self
, ftReceive
, ftHttpPush
):
450 self
.ftReceive
, self
.ftHttpPush
= ftReceive
, ftHttpPush
451 doRateLimit(self
.ftReceive
.legacyftp
.accept
, self
)
453 def write(self
, data
):
454 self
.ftHttpPush
.write(data
)
457 self
.ftHttpPush
.finish()
460 self
.ftHttpPush
.finish()
461 self
.ftReceive
.error()
463 class FileTransferOOBReceive(resource
.Resource
):
464 def __init__(self
, port
):
468 self
.oobSite
= server
.Site(self
)
469 reactor
.listenTCP(port
, self
.oobSite
)
471 def putFile(self
, file, filename
):
472 path
= str(random
.randint(100000000, 999999999))
473 filename
= (path
+ "/" + filename
).replace("//", "/")
474 self
.files
[filename
] = file
477 def remFile(self
, filename
):
478 if self
.files
.has_key(filename
):
479 del self
.files
[filename
]
481 def render_GET(self
, request
):
482 filename
= request
.path
[1:] # Remove the leading /
483 if self
.files
.has_key(filename
):
484 file = self
.files
[filename
]
485 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
486 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
487 OOBReceiveConnector(file, request
)
488 del self
.files
[filename
]
489 return server
.NOT_DONE_YET
491 page
= error
.NoResource(message
="404 File Not Found")
492 return page
.render(request
)
494 def render_HEAD(self
, request
):
495 filename
= request
.path
[1:] # Remove the leading /
496 if self
.files
.has_key(filename
):
497 file = self
.files
[filename
]
498 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
499 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
502 page
= error
.NoResource(message
="404 File Not Found")
503 return page
.render(request
)