]> code.delx.au - pymsnt/blob - src/legacy/msn/msnft.py
5bb0eaa65e1fd6cda6a275966545818934acb615
[pymsnt] / src / legacy / msn / msnft.py
1 # Copyright 2005 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 from twisted.internet import reactor
5 from twisted.internet.defer import Deferred
6 from twisted.protocols.basic import LineReceiver
7 from twisted.internet.protocol import ClientFactory
8 from twisted.python import log
9
10 from msn import checkParamLen, MSNMessage
11
12 from debug import LogEvent, INFO, WARN, ERROR
13 import config
14
15 import random
16
17 MAXAUTHCOOKIE = 2**32-1
18
19
20 class MSNFTReceive_Base:
21 # Public
22 def __init__(self, filename, filesize, userHandle):
23 self.consumer = None
24 self.finished = False
25 self.error = False
26 self.buffer = []
27 self.filename, self.filesize, self.userHandle = filename, filesize, userHandle
28
29 def removeMe(self):
30 self.consumer = None
31
32 def accept(self, yes=True):
33 pass
34
35 def writeTo(self, obj):
36 self.consumer = obj
37 for data in self.buffer:
38 self.consumer.write(data)
39 self.buffer = []
40 if self.finished:
41 self.consumer.close()
42 if self.error:
43 self.consumer.error()
44
45
46 # Private
47 def write(self, data):
48 if self.consumer:
49 self.consumer.write(data)
50 else:
51 self.buffer.append(data)
52
53 def close(self):
54 self.removeMe()
55 self.finished = True
56 if self.consumer:
57 self.consumer.close()
58
59 def gotError(self, ignored=None):
60 self.removeMe()
61 self.error = True
62 if self.consumer:
63 self.consumer.error()
64
65
66 class MSNFTP_Ports:
67 def __init__(self):
68 try:
69 lowPort = int(config.ftLowPort)
70 highPort = int(config.ftHighPort)
71 except ValueError:
72 LogEvent(ERROR, "", "Invalid values for ftLowPort & ftHighPort. Using 6891 & 6899 respectively")
73 lowPort = 6891
74 highPort = 6899
75 self.ports = [lowPort+x for x in xrange(highPort-lowPort)]
76 self.portFree = [True] * len(self.ports)
77
78 def requestPort(self):
79 for i in xrange(len(self.ports)):
80 if self.portFree[i]:
81 self.portFree[i] = False
82 LogEvent(INFO, "", "Reserved a port")
83 return self.ports[i]
84 LogEvent(INFO, "", "Out of ports")
85
86 def freePort(self, port):
87 if self.ports.count(port) > 0:
88 self.portFree[self.ports.index(port)] = True;
89 LogEvent(INFO)
90
91 msnports = MSNFTP_Ports()
92
93 class MSNFTP_Receive(ClientFactory, MSNFTReceive_Base):
94 def __init__(self, filename, filesize, userHandle, iCookie, connectivity, switchboard):
95 MSNFTReceive_Base.__init__(self, filename, filesize, userHandle)
96 self.iCookie = iCookie
97 self.switchboard = switchboard
98 self.serverSocket = None
99 self.timeout = None
100 self.authCookie = str(random.randint(1, MAXAUTHCOOKIE))
101 self.port = None
102 self.d = None
103 LogEvent(INFO, self.switchboard.userHandle)
104
105 def removeMe(self):
106 if self.serverSocket:
107 self.serverSocket.stopListening()
108 if self.timeout and not self.timeout.called:
109 self.timeout.cancel()
110 if self.port:
111 global msnports
112 msnports.freePort(self.port)
113 self.port = None
114 if self.d:
115 self.d.errback()
116 self.d = None
117 LogEvent(INFO, self.switchboard.userHandle)
118
119 def accept(self, yes=True):
120 LogEvent(INFO, self.switchboard.userHandle)
121 global msnports
122 self.port = msnports.requestPort()
123 if not self.port:
124 yes = False
125 self.gotError()
126 LogEvent(INFO, self.switchboard.userHandle)
127
128 from msn import MSNMessage
129 m = MSNMessage()
130 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
131 if yes:
132 m.message += 'IP-Address: %s\r\n' % str(config.ip)
133 m.message += 'Port: %s\r\n' % str(self.port)
134 m.message += 'AuthCookie: %s\r\n' % self.authCookie
135 m.message += 'Sender-Connect: TRUE\r\n'
136 m.message += 'Invitation-Command: ACCEPT\r\n'
137 m.message += 'Invitation-Cookie: %s\r\n' % str(self.iCookie)
138 else:
139 m.message += 'Invitation-Command: CANCEL\r\n'
140 m.message += 'Cancel-Code: REJECT\r\n'
141 m.message += 'Launch-Application: FALSE\r\n'
142 m.message += 'Request-Data: IP-Address:\r\n'
143 m.message += '\r\n'
144 m.ack = m.MESSAGE_ACK_NONE
145 self.switchboard.sendMessage(m)
146
147 if yes:
148 self.d = Deferred()
149 self.serverSocket = reactor.listenTCP(self.port, self)
150 self.timeout = reactor.callLater(20, self.gotError)
151 return self.d
152
153 def buildProtocol(self, addr):
154 LogEvent(INFO, self.switchboard.userHandle)
155 self.serverSocket.stopListening()
156 self.serverSocket = None
157 self.timeout.cancel()
158 self.timeout = None
159 self.d.callback(None)
160 self.d = None
161 return MSNFTP_FileReceive(self.authCookie, self.switchboard.userHandle, self)
162
163
164 class MSNFTP_FileReceive(LineReceiver):
165 """
166 This class provides support for receiving files from contacts.
167
168 @ivar fileSize: the size of the receiving file. (you will have to set this)
169 @ivar connected: true if a connection has been established.
170 @ivar completed: true if the transfer is complete.
171 @ivar bytesReceived: number of bytes (of the file) received.
172 This does not include header data.
173 """
174
175 def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
176 """
177 @param auth: auth string received in the file invitation.
178 @param myUserHandle: your userhandle.
179 @param file: A string or file object represnting the file
180 to save data to.
181 @param directory: optional parameter specifiying the directory.
182 Defaults to the current directory.
183 @param overwrite: if true and a file of the same name exists on
184 your system, it will be overwritten. (0 by default)
185 """
186 self.auth = auth
187 self.myUserHandle = myUserHandle
188 self.fileSize = 0
189 self.connected = 0
190 self.completed = 0
191 self.directory = directory
192 self.bytesReceived = 0
193 self.overwrite = overwrite
194
195 # used for handling current received state
196 self.state = 'CONNECTING'
197 self.segmentLength = 0
198 self.buffer = ''
199
200 if isinstance(file, str):
201 path = os.path.join(directory, file)
202 if os.path.exists(path) and not self.overwrite:
203 log.msg('File already exists...')
204 raise IOError, "File Exists" # is this all we should do here?
205 self.file = open(os.path.join(directory, file), 'wb')
206 else:
207 self.file = file
208
209 def connectionMade(self):
210 self.connected = 1
211 self.state = 'INHEADER'
212 self.sendLine('VER MSNFTP')
213
214 def connectionLost(self, reason):
215 self.connected = 0
216 self.file.close()
217
218 def parseHeader(self, header):
219 """ parse the header of each 'message' to obtain the segment length """
220
221 if ord(header[0]) != 0: # they requested that we close the connection
222 self.transport.loseConnection()
223 return
224 try:
225 extra, factor = header[1:]
226 except ValueError:
227 # munged header, ending transfer
228 self.transport.loseConnection()
229 raise
230 extra = ord(extra)
231 factor = ord(factor)
232 return factor * 256 + extra
233
234 def sendLine(self, line):
235 log.msg("SENDING LINE!!! " + line)
236 LineReceiver.sendLine(self, line)
237
238 def lineReceived(self, line):
239 temp = line.split(' ')
240 if len(temp) == 1: params = []
241 else: params = temp[1:]
242 cmd = temp[0]
243 log.msg("GOT A LINE!!! " + line)
244 handler = getattr(self, "handle_%s" % cmd.upper(), None)
245 if handler: handler(params) # try/except
246 else: self.handle_UNKNOWN(cmd, params)
247
248 def rawDataReceived(self, data):
249 bufferLen = len(self.buffer)
250 log.msg("RAW DATA: " + data)
251 if self.state == 'INHEADER':
252 delim = 3-bufferLen
253 self.buffer += data[:delim]
254 if len(self.buffer) == 3:
255 self.segmentLength = self.parseHeader(self.buffer)
256 if not self.segmentLength: return # hrm
257 self.buffer = ""
258 self.state = 'INSEGMENT'
259 extra = data[delim:]
260 if len(extra) > 0: self.rawDataReceived(extra)
261 return
262
263 elif self.state == 'INSEGMENT':
264 dataSeg = data[:(self.segmentLength-bufferLen)]
265 self.buffer += dataSeg
266 self.bytesReceived += len(dataSeg)
267 if len(self.buffer) == self.segmentLength:
268 self.gotSegment(self.buffer)
269 self.buffer = ""
270 if self.bytesReceived == self.fileSize:
271 self.completed = 1
272 self.buffer = ""
273 self.file.close()
274 self.sendLine("BYE 16777989")
275 return
276 self.state = 'INHEADER'
277 extra = data[(self.segmentLength-bufferLen):]
278 if len(extra) > 0: self.rawDataReceived(extra)
279 return
280
281 def handle_VER(self, params):
282 checkParamLen(len(params), 1, 'VER')
283 if params[0].upper() == "MSNFTP":
284 self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
285 else:
286 log.msg('they sent the wrong version, time to quit this transfer')
287 self.transport.loseConnection()
288
289 def handle_FIL(self, params):
290 checkParamLen(len(params), 1, 'FIL')
291 try:
292 self.fileSize = int(params[0])
293 except ValueError: # they sent the wrong file size - probably want to log this
294 self.transport.loseConnection()
295 return
296 self.setRawMode()
297 self.sendLine("TFR")
298
299 def handle_UNKNOWN(self, cmd, params):
300 log.msg('received unknown command (%s), params: %s' % (cmd, params))
301
302 def gotSegment(self, data):
303 """ called when a segment (block) of data arrives. """
304 self.file.write(data)
305
306
307 class MSNFTP_FileSend(LineReceiver):
308 """
309 This class provides support for sending files to other contacts.
310
311 @ivar bytesSent: the number of bytes that have currently been sent.
312 @ivar completed: true if the send has completed.
313 @ivar connected: true if a connection has been established.
314 @ivar targetUser: the target user (contact).
315 @ivar segmentSize: the segment (block) size.
316 @ivar auth: the auth cookie (number) to use when sending the
317 transfer invitation
318 """
319
320 def __init__(self, file):
321 """
322 @param file: A string or file object represnting the file to send.
323 """
324
325 if isinstance(file, str) or isinstance(file, unicode):
326 self.file = open(file, 'rb')
327 else:
328 self.file = file
329
330 self.fileSize = 0
331 self.bytesSent = 0
332 self.completed = 0
333 self.connected = 0
334 self.targetUser = None
335 self.segmentSize = 2045
336 self.auth = random.randint(0, 2**30)
337 self._pendingSend = None # :(
338
339 def connectionMade(self):
340 self.connected = 1
341
342 def connectionLost(self, reason):
343 if self._pendingSend:
344 self._pendingSend.cancel()
345 self._pendingSend = None
346 self.connected = 0
347 self.file.close()
348
349 def lineReceived(self, line):
350 temp = line.split(' ')
351 if len(temp) == 1: params = []
352 else: params = temp[1:]
353 cmd = temp[0]
354 handler = getattr(self, "handle_%s" % cmd.upper(), None)
355 if handler: handler(params)
356 else: self.handle_UNKNOWN(cmd, params)
357
358 def handle_VER(self, params):
359 checkParamLen(len(params), 1, 'VER')
360 if params[0].upper() == "MSNFTP":
361 self.sendLine("VER MSNFTP")
362 else: # they sent some weird version during negotiation, i'm quitting.
363 self.transport.loseConnection()
364
365 def handle_USR(self, params):
366 checkParamLen(len(params), 2, 'USR')
367 self.targetUser = params[0]
368 if self.auth == int(params[1]):
369 self.sendLine("FIL %s" % (self.fileSize))
370 else: # they failed the auth test, disconnecting.
371 self.transport.loseConnection()
372
373 def handle_TFR(self, params):
374 checkParamLen(len(params), 0, 'TFR')
375 # they are ready for me to start sending
376 self.sendPart()
377
378 def handle_BYE(self, params):
379 self.completed = (self.bytesSent == self.fileSize)
380 self.transport.loseConnection()
381
382 def handle_CCL(self, params):
383 self.completed = (self.bytesSent == self.fileSize)
384 self.transport.loseConnection()
385
386 def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params))
387
388 def makeHeader(self, size):
389 """ make the appropriate header given a specific segment size. """
390 quotient, remainder = divmod(size, 256)
391 return chr(0) + chr(remainder) + chr(quotient)
392
393 def sendPart(self):
394 """ send a segment of data """
395 if not self.connected:
396 self._pendingSend = None
397 return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
398 data = self.file.read(self.segmentSize)
399 if data:
400 dataSize = len(data)
401 header = self.makeHeader(dataSize)
402 self.transport.write(header + data)
403 self.bytesSent += dataSize
404 self._pendingSend = reactor.callLater(0, self.sendPart)
405 else:
406 self._pendingSend = None
407 self.completed = 1