]>
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 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):
282 from tlib
import socks5
285 class JEP65ConnectionSend(protocol
.Protocol
):
286 # TODO, clean up and move this to tlib.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 def _waitAuthOk(self
):
305 ver
, method
= struct
.unpack("!BB", self
.buf
[:2])
306 if ver
!= 5 or method
!= 0:
307 self
.transport
.loseConnection()
309 self
.buf
= self
.buf
[2:] # chop
311 # Send CONNECT request
312 length
= len(self
.factory
.hash)
313 self
.transport
.write(struct
.pack("!BBBBB", 5, 1, 0, 3, length
))
314 self
.transport
.write("".join([struct
.pack("!B" , ord(x
))[0] for x
in self
.factory
.hash]))
315 self
.transport
.write(struct
.pack("!H", 0))
316 self
.state
= self
.STATE_WAIT_CONNECTOK
318 def _waitConnectOk(self
):
319 ver
, rep
, rsv
, atyp
= struct
.unpack("!BBBB", self
.buf
[:4])
320 if not (ver
== 5 and rep
== 0):
321 self
.transport
.loseConnection()
324 self
.state
= self
.STATE_READY
325 self
.factory
.madeConnection(self
.transport
.addr
[0])
327 def dataReceived(self
, buf
):
328 if self
.state
== self
.STATE_READY
:
329 self
.factory
.consumer
.write(buf
)
332 if self
.state
== self
.STATE_WAIT_AUTHOK
:
334 elif self
.state
== self
.STATE_WAIT_CONNECTOK
:
335 self
._waitConnectOk
()
338 class JEP65ConnectionReceive(socks5
.SOCKSv5
):
339 def __init__(self
, listener
):
340 socks5
.SOCKSv5
.__init__(self
)
341 self
.listener
= listener
342 self
.supportedAuthMechs
= [socks5
.AUTHMECH_ANON
]
343 self
.supportedAddrs
= [socks5
.ADDR_DOMAINNAME
]
344 self
.enabledCommands
= [socks5
.CMD_CONNECT
]
347 def connectRequested(self
, addr
, port
):
348 # So that the legacyftp can close the connection
349 self
.transport
.close
= self
.transport
.loseConnection
351 # Check for special connect to the namespace -- this signifies that
352 # the client is just checking that it can connect to the streamhost
353 if addr
== disco
.S5B
:
354 self
.connectCompleted(addr
, 0)
355 self
.transport
.loseConnection()
360 if self
.listener
.isActive(addr
):
361 self
.sendErrorReply(socks5
.REPLY_CONN_NOT_ALLOWED
)
364 if self
.listener
.addConnection(addr
, self
):
365 self
.connectCompleted(addr
, 0)
367 self
.sendErrorReply(socks5
.REPLY_CONN_REFUSED
)
369 def connectionLost(self
, reason
):
370 if self
.state
== socks5
.STATE_CONNECT_PENDING
:
371 self
.listener
.removePendingConnection(self
.addr
, self
)
373 self
.transport
.unregisterProducer()
374 if self
.peersock
!= None:
375 self
.peersock
.peersock
= None
376 self
.peersock
.transport
.unregisterProducer()
378 self
.listener
.removeActiveConnection(self
.addr
)
380 class Proxy65(protocol
.Factory
):
381 def __init__(self
, port
):
383 reactor
.listenTCP(port
, self
)
384 self
.pendingConns
= {}
385 self
.activeConns
= {}
387 def buildProtocol(self
, addr
):
388 return JEP65ConnectionReceive(self
)
390 def isActive(self
, address
):
391 return address
in self
.activeConns
393 def activateStream(self
, address
):
394 if address
in self
.pendingConns
:
395 olist
= self
.pendingConns
[address
]
397 LogEvent(WARN
, '', "Not exactly two!")
400 assert address
not in self
.activeConns
401 self
.activeConns
[address
] = None
403 if not isinstance(olist
[0], JEP65ConnectionReceive
):
405 connection
= olist
[1]
406 elif not isinstance(olist
[1], JEP65ConnectionReceive
):
408 connection
= olist
[0]
410 LogEvent(WARN
, '', "No JEP65Connection")
413 doRateLimit(legacyftp
.accept
, connection
.transport
)
415 LogEvent(WARN
, '', "No pending connection.")
417 def addConnection(self
, address
, connection
):
418 olist
= self
.pendingConns
.get(address
, [])
420 olist
.append(connection
)
421 self
.pendingConns
[address
] = olist
423 self
.activateStream(address
)
428 def removePendingConnection(self
, address
, connection
):
429 olist
= self
.pendingConns
[address
]
431 del self
.pendingConns
[address
]
433 olist
.remove(connection
)
435 def removeActiveConnection(self
, address
):
436 del self
.activeConns
[address
]
439 # OOB download server
441 from twisted
.web
import server
, resource
, error
442 from twisted
.internet
import reactor
444 from debug
import LogEvent
, INFO
, WARN
, ERROR
446 class OOBReceiveConnector
:
447 def __init__(self
, ftReceive
, ftHttpPush
):
448 self
.ftReceive
, self
.ftHttpPush
= ftReceive
, ftHttpPush
449 doRateLimit(self
.ftReceive
.legacyftp
.accept
, self
)
451 def write(self
, data
):
452 self
.ftHttpPush
.write(data
)
455 self
.ftHttpPush
.finish()
458 self
.ftHttpPush
.finish()
459 self
.ftReceive
.error()
461 class FileTransferOOBReceive(resource
.Resource
):
462 def __init__(self
, port
):
466 self
.oobSite
= server
.Site(self
)
467 reactor
.listenTCP(port
, self
.oobSite
)
469 def putFile(self
, file, filename
):
470 path
= str(random
.randint(100000000, 999999999))
471 filename
= (path
+ "/" + filename
).replace("//", "/")
472 self
.files
[filename
] = file
475 def remFile(self
, filename
):
476 if self
.files
.has_key(filename
):
477 del self
.files
[filename
]
479 def render_GET(self
, request
):
480 filename
= request
.path
[1:] # Remove the leading /
481 if self
.files
.has_key(filename
):
482 file = self
.files
[filename
]
483 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
484 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
485 OOBReceiveConnector(file, request
)
486 del self
.files
[filename
]
487 return server
.NOT_DONE_YET
489 page
= error
.NoResource(message
="404 File Not Found")
490 return page
.render(request
)
492 def render_HEAD(self
, request
):
493 filename
= request
.path
[1:] # Remove the leading /
494 if self
.files
.has_key(filename
):
495 file = self
.files
[filename
]
496 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
497 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
500 page
= error
.NoResource(message
="404 File Not Found")
501 return page
.render(request
)