]>
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
9 from debug
import LogEvent
, INFO
, WARN
, ERROR
17 def checkSizeOk(size
):
20 limit
= int(config
.ftSizeLimit
)
32 """ For file transfers going from Jabber to MSN. """
33 def __init__(self
, session
, to
, startTransfer
, cancelTransfer
, filename
, filesize
):
34 self
.startTransfer
= startTransfer
35 self
.cancelTransfer
= cancelTransfer
36 self
.filename
= filename
37 self
.filesize
= filesize
38 if not checkSizeOk(self
.filesize
):
39 LogEvent(INFO
, session
.jabberID
, "File too large.")
40 session
.legacycon
.sendMessage(to
, "", lang
.get(session
.lang
).msnFtSizeRejected
% (self
.filename
, config
.ftSizeLimit
, config
.website
), True)
44 session
.legacycon
.sendFile(to
, self
)
46 def accept(self
, legacyFileSend
):
47 self
.startTransfer(legacyFileSend
)
50 del self
.startTransfer
55 from twisted
.web
import http
58 from twisted
.protocols
import http
60 print "Couldn't find http.HTTPClient. If you're using Twisted 2.0, make sure that you've installed twisted.web"
64 class OOBHeaderHelper(http
.HTTPClient
):
65 """ Makes a HEAD request and grabs the length """
66 def connectionMade(self
):
67 self
.sendCommand("HEAD", self
.factory
.path
.encode("utf-8"))
68 self
.sendHeader("Host", (self
.factory
.host
+ ":" + str(self
.factory
.port
)).encode("utf-8"))
71 def handleEndHeaders(self
):
72 self
.factory
.gotLength(self
.length
)
74 def handleResponse(self
, data
):
78 class OOBSendConnector(http
.HTTPClient
):
79 def connectionMade(self
):
80 self
.sendCommand("GET", self
.factory
.path
.encode("utf-8"))
81 self
.sendHeader("Host", (self
.factory
.host
+ ":" + str(self
.factory
.port
)).encode("utf-8"))
85 def handleResponsePart(self
, data
):
86 self
.factory
.consumer
.write(data
)
88 def handleResponseEnd(self
):
89 # This is called once before writing is finished, and once when the
90 # connection closes. We only consumer.close() on the second.
94 self
.factory
.consumer
.close()
95 self
.factory
.consumer
= None
96 self
.factory
.finished()
107 """ For file transfers going from MSN to Jabber. """
110 Plan of action for this class:
111 * Determine the FT support of the Jabber client.
112 * If we find a common protocol, then send the invitation.
113 * Tell the legacyftp object the result of the invitation.
114 * If it was accepted, then start the transfer.
118 def __init__(self
, session
, senderJID
, legacyftp
):
119 if not checkSizeOk(legacyftp
.filesize
):
120 LogEvent(INFO
, session
.jabberID
, "File too large.")
122 session
.legacycon
.sendMessage(senderJID
, "", lang
.get(session
.lang
).msnFtSizeRejected
% (legacyftp
.filename
, config
.ftSizeLimit
, config
.website
), False)
124 self
.session
= session
125 self
.toJID
= self
.session
.jabberID
+ "/" + self
.session
.highestResource()
126 self
.senderJID
= senderJID
127 self
.ident
= (self
.toJID
, self
.senderJID
)
128 self
.legacyftp
= legacyftp
129 LogEvent(INFO
, session
.jabberID
)
132 def checkSupport(self
):
133 def discoDone(features
):
134 LogEvent(INFO
, self
.ident
)
135 enabledS5B
= hasattr(self
.session
.pytrans
, "ftSOCKS5Receive")
136 enabledOOB
= hasattr(self
.session
.pytrans
, "ftOOBReceive")
137 hasFT
= features
.count(disco
.FT
)
138 hasS5B
= features
.count(disco
.S5B
)
139 hasOOB
= features
.count(disco
.IQOOB
)
140 LogEvent(INFO
, self
.ident
, "Choosing transfer mode.")
141 if hasFT
> 0 and hasS5B
> 0 and enabledS5B
:
143 elif hasOOB
> 0 and enabledOOB
:
146 self
.messageOobMode()
149 self
.legacyftp
.reject()
152 def discoFail(err
=None):
153 LogEvent(INFO
, self
.ident
, str(err
))
154 if hasattr(self
.session
.pytrans
, "ftOOBReceive"):
155 self
.messageOobMode()
158 self
.legacyftp
.reject()
161 d
= disco
.DiscoRequest(self
.session
.pytrans
, self
.toJID
).doDisco()
162 d
.addCallbacks(discoDone
, discoFail
)
166 if el
.getAttribute("type") != "result":
169 self
.session
.pytrans
.ftSOCKS5Receive
.addConnection(utils
.socks5Hash(self
.sid
, self
.senderJID
, self
.toJID
), self
.legacyftp
)
170 LogEvent(INFO
, self
.ident
)
171 iq
= Element((None, "iq"))
172 iq
.attributes
["type"] = "set"
173 iq
.attributes
["to"] = self
.toJID
174 iq
.attributes
["from"] = self
.senderJID
175 query
= iq
.addElement("query")
176 query
.attributes
["xmlns"] = disco
.S5B
177 query
.attributes
["sid"] = self
.sid
178 query
.attributes
["mode"] = "tcp"
179 streamhost
= query
.addElement("streamhost")
180 streamhost
.attributes
["jid"] = self
.senderJID
181 streamhost
.attributes
["host"] = config
.host
182 streamhost
.attributes
["port"] = config
.ftJabberPort
183 d
= self
.session
.pytrans
.discovery
.sendIq(iq
)
184 d
.addErrback(ftDeclined
) # Timeout
187 self
.legacyftp
.reject()
190 LogEvent(INFO
, self
.ident
)
191 self
.sid
= str(random
.randint(1000, sys
.maxint
))
192 iq
= Element((None, "iq"))
193 iq
.attributes
["type"] = "set"
194 iq
.attributes
["to"] = self
.toJID
195 iq
.attributes
["from"] = self
.senderJID
196 si
= iq
.addElement("si")
197 si
.attributes
["xmlns"] = disco
.SI
198 si
.attributes
["profile"] = disco
.FT
199 si
.attributes
["id"] = self
.sid
200 file = si
.addElement("file")
201 file.attributes
["xmlns"] = disco
.FT
202 file.attributes
["size"] = str(self
.legacyftp
.filesize
)
203 file.attributes
["name"] = self
.legacyftp
.filename
204 # Feature negotiation
205 feature
= si
.addElement("feature")
206 feature
.attributes
["xmlns"] = disco
.FEATURE_NEG
207 x
= feature
.addElement("x")
208 x
.attributes
["xmlns"] = disco
.XDATA
209 x
.attributes
["type"] = "form"
210 field
= x
.addElement("field")
211 field
.attributes
["type"] = "list-single"
212 field
.attributes
["var"] = "stream-method"
213 option
= field
.addElement("option")
214 value
= option
.addElement("value")
215 value
.addContent(disco
.S5B
)
216 d
= self
.session
.pytrans
.discovery
.sendIq(iq
, 60*3)
217 d
.addCallback(ftReply
)
218 d
.addErrback(ftDeclined
)
222 if el
.getAttribute("type") != "result":
223 self
.legacyftp
.reject()
225 self
.session
.pytrans
.ftOOBReceive
.remFile(filename
)
227 def ecb(ignored
=None):
228 self
.legacyftp
.reject()
231 LogEvent(INFO
, self
.ident
)
232 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
233 iq
= Element((None, "iq"))
234 iq
.attributes
["to"] = self
.toJID
235 iq
.attributes
["from"] = self
.senderJID
236 query
= m
.addElement("query")
237 query
.attributes
["xmlns"] = disco
.IQOOB
238 query
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
239 d
= self
.session
.send(iq
)
240 d
.addCallbacks(cb
, ecb
)
242 def messageOobMode(self
):
243 LogEvent(INFO
, self
.ident
)
244 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
245 m
= Element((None, "message"))
246 m
.attributes
["to"] = self
.session
.jabberID
247 m
.attributes
["from"] = self
.senderJID
248 m
.addElement("body").addContent(config
.ftOOBRoot
+ "/" + filename
)
249 x
= m
.addElement("x")
250 x
.attributes
["xmlns"] = disco
.XOOB
251 x
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
252 self
.session
.pytrans
.send(m
)
254 def error(self
, ignored
=None):
262 from tlib
import socks5
265 class JEP65ConnectionSend(protocol
.Protocol
):
266 # TODO, clean up and move this to tlib.socks5
268 STATE_WAIT_AUTHOK
= 2
269 STATE_WAIT_CONNECTOK
= 3
273 self
.state
= self
.STATE_INITIAL
276 def connectionMade(self
):
277 self
.transport
.write(struct
.pack("!BBB", 5, 1, 0))
278 self
.state
= self
.STATE_WAIT_AUTHOK
280 def connectionLost(self
, reason
):
281 if self
.state
== self
.STATE_READY
:
282 self
.factory
.consumer
.close()
284 def _waitAuthOk(self
):
285 ver
, method
= struct
.unpack("!BB", self
.buf
[:2])
286 if ver
!= 5 or method
!= 0:
287 self
.transport
.loseConnection()
289 self
.buf
= self
.buf
[2:] # chop
291 # Send CONNECT request
292 length
= len(self
.factory
.hash)
293 self
.transport
.write(struct
.pack("!BBBBB", 5, 1, 0, 3, length
))
294 self
.transport
.write("".join([struct
.pack("!B" , ord(x
))[0] for x
in self
.factory
.hash]))
295 self
.transport
.write(struct
.pack("!H", 0))
296 self
.state
= self
.STATE_WAIT_CONNECTOK
298 def _waitConnectOk(self
):
299 ver
, rep
, rsv
, atyp
= struct
.unpack("!BBBB", self
.buf
[:4])
300 if not (ver
== 5 and rep
== 0):
301 self
.transport
.loseConnection()
304 self
.state
= self
.STATE_READY
305 self
.factory
.madeConnection(self
.transport
.addr
[0])
307 def dataReceived(self
, buf
):
308 if self
.state
== self
.STATE_READY
:
309 self
.factory
.consumer
.write(buf
)
312 if self
.state
== self
.STATE_WAIT_AUTHOK
:
314 elif self
.state
== self
.STATE_WAIT_CONNECTOK
:
315 self
._waitConnectOk
()
318 class JEP65ConnectionReceive(socks5
.SOCKSv5
):
319 def __init__(self
, listener
):
320 socks5
.SOCKSv5
.__init__(self
)
321 self
.listener
= listener
322 self
.supportedAuthMechs
= [socks5
.AUTHMECH_ANON
]
323 self
.supportedAddrs
= [socks5
.ADDR_DOMAINNAME
]
324 self
.enabledCommands
= [socks5
.CMD_CONNECT
]
327 def connectRequested(self
, addr
, port
):
328 # So that the legacyftp can close the connection
329 self
.transport
.close
= self
.transport
.loseConnection
331 # Check for special connect to the namespace -- this signifies that
332 # the client is just checking that it can connect to the streamhost
333 if addr
== disco
.S5B
:
334 self
.connectCompleted(addr
, 0)
335 self
.transport
.loseConnection()
340 if self
.listener
.isActive(addr
):
341 self
.sendErrorReply(socks5
.REPLY_CONN_NOT_ALLOWED
)
344 if self
.listener
.addConnection(addr
, self
):
345 self
.connectCompleted(addr
, 0)
347 self
.sendErrorReply(socks5
.REPLY_CONN_REFUSED
)
349 def connectionLost(self
, reason
):
350 if self
.state
== socks5
.STATE_CONNECT_PENDING
:
351 self
.listener
.removePendingConnection(self
.addr
, self
)
353 self
.transport
.unregisterProducer()
354 if self
.peersock
!= None:
355 self
.peersock
.peersock
= None
356 self
.peersock
.transport
.unregisterProducer()
358 self
.listener
.removeActiveConnection(self
.addr
)
360 class Proxy65(protocol
.Factory
):
361 def __init__(self
, port
):
363 reactor
.listenTCP(port
, self
)
364 self
.pendingConns
= {}
365 self
.activeConns
= {}
367 def buildProtocol(self
, addr
):
368 return JEP65ConnectionReceive(self
)
370 def isActive(self
, address
):
371 return address
in self
.activeConns
373 def activateStream(self
, address
):
374 if address
in self
.pendingConns
:
375 olist
= self
.pendingConns
[address
]
377 LogEvent(WARN
, '', "Not exactly two!")
380 assert address
not in self
.activeConns
381 self
.activeConns
[address
] = None
383 if not isinstance(olist
[0], (JEP65ConnectionReceive
, JEP65ConnectionSend
)):
385 connection
= olist
[1]
386 elif not isinstance(olist
[1], (JEP65ConnectionReceive
, JEP65ConnectionSend
)):
388 connection
= olist
[0]
390 LogEvent(WARN
, '', "No JEP65Connection")
393 legacyftp
.accept(connection
.transport
)
395 LogEvent(WARN
, '', "No pending connection.")
397 def addConnection(self
, address
, connection
):
398 olist
= self
.pendingConns
.get(address
, [])
400 olist
.append(connection
)
401 self
.pendingConns
[address
] = olist
403 self
.activateStream(address
)
408 def removePendingConnection(self
, address
, connection
):
409 olist
= self
.pendingConns
[address
]
411 del self
.pendingConns
[address
]
413 olist
.remove(connection
)
415 def removeActiveConnection(self
, address
):
416 del self
.activeConns
[address
]
419 # OOB download server
421 from twisted
.web
import server
, resource
, error
422 from twisted
.internet
import reactor
424 from debug
import LogEvent
, INFO
, WARN
, ERROR
426 class OOBReceiveConnector
:
427 def __init__(self
, ftReceive
, ftHttpPush
):
428 self
.ftReceive
, self
.ftHttpPush
= ftReceive
, ftHttpPush
429 self
.ftReceive
.legacyftp
.accept(self
)
431 def write(self
, data
):
432 self
.ftHttpPush
.write(data
)
435 self
.ftHttpPush
.finish()
438 self
.ftHttpPush
.finish()
439 self
.ftReceive
.error()
441 class FileTransferOOBReceive(resource
.Resource
):
442 def __init__(self
, port
):
446 self
.oobSite
= server
.Site(self
)
447 reactor
.listenTCP(port
, self
.oobSite
)
449 def putFile(self
, file, filename
):
450 path
= str(random
.randint(100000000, 999999999))
451 filename
= (path
+ "/" + filename
).replace("//", "/")
452 self
.files
[filename
] = file
455 def remFile(self
, filename
):
456 if self
.files
.has_key(filename
):
457 del self
.files
[filename
]
459 def render_GET(self
, request
):
460 filename
= request
.path
[1:] # Remove the leading /
461 if self
.files
.has_key(filename
):
462 file = self
.files
[filename
]
463 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
464 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
465 OOBReceiveConnector(file, request
)
466 del self
.files
[filename
]
467 return server
.NOT_DONE_YET
469 page
= error
.NoResource(message
="404 File Not Found")
470 return page
.render(request
)
472 def render_HEAD(self
, request
):
473 filename
= request
.path
[1:] # Remove the leading /
474 if self
.files
.has_key(filename
):
475 file = self
.files
[filename
]
476 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
477 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
480 page
= error
.NoResource(message
="404 File Not Found")
481 return page
.render(request
)