]>
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
):
18 if not config
.ftSizeLimit
:
19 return True # No limit
22 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 self
.messageOobMode()
156 d
= disco
.DiscoRequest(self
.session
.pytrans
, self
.toJID
).doDisco()
157 d
.addCallbacks(discoDone
, discoFail
)
161 if el
.getAttribute("type") != "result":
164 self
.session
.pytrans
.ftSOCKS5Receive
.addConnection(utils
.socks5Hash(self
.sid
, self
.senderJID
, self
.toJID
), self
.legacyftp
)
165 LogEvent(INFO
, self
.ident
)
166 iq
= Element((None, "iq"))
167 iq
.attributes
["type"] = "set"
168 iq
.attributes
["to"] = self
.toJID
169 iq
.attributes
["from"] = self
.senderJID
170 query
= iq
.addElement("query")
171 query
.attributes
["xmlns"] = disco
.S5B
172 query
.attributes
["sid"] = self
.sid
173 query
.attributes
["mode"] = "tcp"
174 streamhost
= query
.addElement("streamhost")
175 streamhost
.attributes
["jid"] = self
.senderJID
176 streamhost
.attributes
["host"] = config
.host
177 streamhost
.attributes
["port"] = config
.ftJabberPort
178 d
= self
.session
.pytrans
.discovery
.sendIq(iq
)
179 d
.addErrback(ftDeclined
) # Timeout
182 self
.legacyftp
.reject()
185 LogEvent(INFO
, self
.ident
)
186 self
.sid
= str(random
.randint(1000, sys
.maxint
))
187 iq
= Element((None, "iq"))
188 iq
.attributes
["type"] = "set"
189 iq
.attributes
["to"] = self
.toJID
190 iq
.attributes
["from"] = self
.senderJID
191 si
= iq
.addElement("si")
192 si
.attributes
["xmlns"] = disco
.SI
193 si
.attributes
["profile"] = disco
.FT
194 si
.attributes
["id"] = self
.sid
195 file = si
.addElement("file")
196 file.attributes
["xmlns"] = disco
.FT
197 file.attributes
["size"] = str(self
.legacyftp
.filesize
)
198 file.attributes
["name"] = self
.legacyftp
.filename
199 # Feature negotiation
200 feature
= si
.addElement("feature")
201 feature
.attributes
["xmlns"] = disco
.FEATURE_NEG
202 x
= feature
.addElement("x")
203 x
.attributes
["xmlns"] = disco
.XDATA
204 x
.attributes
["type"] = "form"
205 field
= x
.addElement("field")
206 field
.attributes
["type"] = "list-single"
207 field
.attributes
["var"] = "stream-method"
208 option
= field
.addElement("option")
209 value
= option
.addElement("value")
210 value
.addContent(disco
.S5B
)
211 d
= self
.session
.pytrans
.discovery
.sendIq(iq
, 60*3)
212 d
.addCallback(ftReply
)
213 d
.addErrback(ftDeclined
)
217 if el
.getAttribute("type") != "result":
218 self
.legacyftp
.reject()
220 self
.session
.pytrans
.ftOOBReceive
.remFile(filename
)
222 def ecb(ignored
=None):
223 self
.legacyftp
.reject()
226 LogEvent(INFO
, self
.ident
)
227 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
228 iq
= Element((None, "iq"))
229 iq
.attributes
["to"] = self
.toJID
230 iq
.attributes
["from"] = self
.senderJID
231 query
= m
.addElement("query")
232 query
.attributes
["xmlns"] = disco
.IQOOB
233 query
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
234 d
= self
.session
.send(iq
)
235 d
.addCallbacks(cb
, ecb
)
237 def messageOobMode(self
):
238 LogEvent(INFO
, self
.ident
)
239 filename
= self
.session
.pytrans
.ftOOBReceive
.putFile(self
, self
.legacyftp
.filename
)
240 m
= Element((None, "message"))
241 m
.attributes
["to"] = self
.session
.jabberID
242 m
.attributes
["from"] = self
.senderJID
243 m
.addElement("body").addContent(config
.ftOOBRoot
+ "/" + filename
)
244 x
= m
.addElement("x")
245 x
.attributes
["xmlns"] = disco
.XOOB
246 x
.addElement("url").addContent(config
.ftOOBRoot
+ "/" + filename
)
247 self
.session
.pytrans
.send(m
)
249 def error(self
, ignored
=None):
257 from tlib
import socks5
260 class JEP65ConnectionSend(protocol
.Protocol
):
261 # TODO, clean up and move this to tlib.socks5
263 STATE_WAIT_AUTHOK
= 2
264 STATE_WAIT_CONNECTOK
= 3
268 self
.state
= self
.STATE_INITIAL
271 def connectionMade(self
):
272 self
.transport
.write(struct
.pack("!BBB", 5, 1, 0))
273 self
.state
= self
.STATE_WAIT_AUTHOK
275 def connectionLost(self
, reason
):
276 if self
.state
== self
.STATE_READY
:
277 self
.factory
.consumer
.close()
279 def _waitAuthOk(self
):
280 ver
, method
= struct
.unpack("!BB", self
.buf
[:2])
281 if ver
!= 5 or method
!= 0:
282 self
.transport
.loseConnection()
284 self
.buf
= self
.buf
[2:] # chop
286 # Send CONNECT request
287 length
= len(self
.factory
.hash)
288 self
.transport
.write(struct
.pack("!BBBBB", 5, 1, 0, 3, length
))
289 self
.transport
.write("".join([struct
.pack("!B" , ord(x
))[0] for x
in self
.factory
.hash]))
290 self
.transport
.write(struct
.pack("!H", 0))
291 self
.state
= self
.STATE_WAIT_CONNECTOK
293 def _waitConnectOk(self
):
294 ver
, rep
, rsv
, atyp
= struct
.unpack("!BBBB", self
.buf
[:4])
295 if not (ver
== 5 and rep
== 0):
296 self
.transport
.loseConnection()
299 self
.state
= self
.STATE_READY
300 self
.factory
.madeConnection(self
.transport
.addr
[0])
302 def dataReceived(self
, buf
):
303 if self
.state
== self
.STATE_READY
:
304 self
.factory
.consumer
.write(buf
)
307 if self
.state
== self
.STATE_WAIT_AUTHOK
:
309 elif self
.state
== self
.STATE_WAIT_CONNECTOK
:
310 self
._waitConnectOk
()
313 class JEP65ConnectionReceive(socks5
.SOCKSv5
):
314 def __init__(self
, listener
):
315 socks5
.SOCKSv5
.__init__(self
)
316 self
.listener
= listener
317 self
.supportedAuthMechs
= [socks5
.AUTHMECH_ANON
]
318 self
.supportedAddrs
= [socks5
.ADDR_DOMAINNAME
]
319 self
.enabledCommands
= [socks5
.CMD_CONNECT
]
322 def connectRequested(self
, addr
, port
):
323 # So that the legacyftp can close the connection
324 self
.transport
.close
= self
.transport
.loseConnection
326 # Check for special connect to the namespace -- this signifies that
327 # the client is just checking that it can connect to the streamhost
328 if addr
== disco
.S5B
:
329 self
.connectCompleted(addr
, 0)
330 self
.transport
.loseConnection()
335 if self
.listener
.isActive(addr
):
336 self
.sendErrorReply(socks5
.REPLY_CONN_NOT_ALLOWED
)
339 if self
.listener
.addConnection(addr
, self
):
340 self
.connectCompleted(addr
, 0)
342 self
.sendErrorReply(socks5
.REPLY_CONN_REFUSED
)
344 def connectionLost(self
, reason
):
345 if self
.state
== socks5
.STATE_CONNECT_PENDING
:
346 self
.listener
.removePendingConnection(self
.addr
, self
)
348 self
.transport
.unregisterProducer()
349 if self
.peersock
!= None:
350 self
.peersock
.peersock
= None
351 self
.peersock
.transport
.unregisterProducer()
353 self
.listener
.removeActiveConnection(self
.addr
)
355 class Proxy65(protocol
.Factory
):
356 def __init__(self
, port
):
358 reactor
.listenTCP(port
, self
)
359 self
.pendingConns
= {}
360 self
.activeConns
= {}
362 def buildProtocol(self
, addr
):
363 return JEP65ConnectionReceive(self
)
365 def isActive(self
, address
):
366 return address
in self
.activeConns
368 def activateStream(self
, address
):
369 if address
in self
.pendingConns
:
370 olist
= self
.pendingConns
[address
]
372 LogEvent(WARN
, '', "Not exactly two!")
375 assert address
not in self
.activeConns
376 self
.activeConns
[address
] = None
378 if not isinstance(olist
[0], (JEP65ConnectionReceive
, JEP65ConnectionSend
)):
380 connection
= olist
[1]
381 elif not isinstance(olist
[1], (JEP65ConnectionReceive
, JEP65ConnectionSend
)):
383 connection
= olist
[0]
385 LogEvent(WARN
, '', "No JEP65Connection")
388 legacyftp
.accept(connection
.transport
)
390 LogEvent(WARN
, '', "No pending connection.")
392 def addConnection(self
, address
, connection
):
393 olist
= self
.pendingConns
.get(address
, [])
395 olist
.append(connection
)
396 self
.pendingConns
[address
] = olist
398 self
.activateStream(address
)
403 def removePendingConnection(self
, address
, connection
):
404 olist
= self
.pendingConns
[address
]
406 del self
.pendingConns
[address
]
408 olist
.remove(connection
)
410 def removeActiveConnection(self
, address
):
411 del self
.activeConns
[address
]
414 # OOB download server
416 from twisted
.web
import server
, resource
, error
417 from twisted
.internet
import reactor
419 from debug
import LogEvent
, INFO
, WARN
, ERROR
421 class OOBReceiveConnector
:
422 def __init__(self
, ftReceive
, ftHttpPush
):
423 self
.ftReceive
, self
.ftHttpPush
= ftReceive
, ftHttpPush
424 self
.ftReceive
.legacyftp
.accept(self
)
426 def write(self
, data
):
427 self
.ftHttpPush
.write(data
)
430 self
.ftHttpPush
.finish()
433 self
.ftHttpPush
.finish()
434 self
.ftReceive
.error()
436 class FileTransferOOBReceive(resource
.Resource
):
437 def __init__(self
, port
):
441 self
.oobSite
= server
.Site(self
)
442 reactor
.listenTCP(port
, self
.oobSite
)
444 def putFile(self
, file, filename
):
445 path
= str(random
.randint(100000000, 999999999))
446 filename
= (path
+ "/" + filename
).replace("//", "/")
447 self
.files
[filename
] = file
450 def remFile(self
, filename
):
451 if self
.files
.has_key(filename
):
452 del self
.files
[filename
]
454 def render_GET(self
, request
):
455 filename
= request
.path
[1:] # Remove the leading /
456 if self
.files
.has_key(filename
):
457 file = self
.files
[filename
]
458 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
459 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
460 OOBReceiveConnector(file, request
)
461 del self
.files
[filename
]
462 return server
.NOT_DONE_YET
464 page
= error
.NoResource(message
="404 File Not Found")
465 return page
.render(request
)
467 def render_HEAD(self
, request
):
468 filename
= request
.path
[1:] # Remove the leading /
469 if self
.files
.has_key(filename
):
470 file = self
.files
[filename
]
471 request
.setHeader("Content-Length", str(file.legacyftp
.filesize
))
472 request
.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp
.filename
.encode("utf-8"))
475 page
= error
.NoResource(message
="404 File Not Found")
476 return page
.render(request
)