blob: b360942ee50653f555608daffad5fe218c9ea196 [file] [log] [blame]
Guido van Rossum1115ab21992-11-04 15:51:30 +00001# An FTP client class. Based on RFC 959: File Transfer Protocol
2# (FTP), by J. Postel and J. Reynolds
3
4
5import os
6import sys
7import socket
8import string
9
10
11# Default port numbers used by the FTP protocol
12FTP_PORT = 21
13FTP_DATA_PORT = 20
14
15
16# Exception raiseds when an error or invalid response is received
17error_reply = 'nntp.error_reply' # unexpected [123]xx reply
18error_function = 'nntp.error_function' # 4xx errors
19error_form = 'nntp.error_form' # 5xx errors
20error_protocol = 'nntp.error_protocol' # response does not begin with [1-5]
21
22
23# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
24CRLF = '\r\n'
25
26
27# Next port to be used by makeport(), with PORT_OFFSET added
28nextport = 0
29PORT_OFFSET = 40000
30PORT_CYCLE = 1000
31# XXX This is a nuisance: when using the program several times in a row,
32# reusing the port doesn't work and you have to edit the first port
33# assignment...
34
35
36# The class itself
37class FTP:
38
39 # Initialize an instance. Arguments:
40 # - host: hostname to connect to
41 # - port: port to connect to (default the standard FTP port)
42 def init(self, host, *args):
43 if len(args) > 1: raise TypeError, 'too many args'
44 if args: port = args[0]
45 else: port = FTP_PORT
46 self.host = host
47 self.port = port
48 self.debugging = 0
49 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
50 self.sock.connect(self.host, self.port)
51 self.file = self.sock.makefile('r')
52 self.welcome = self.getresp()
53 return self
54
55 # Get the welcome message from the server
56 # (this is read and squirreled away by init())
57 def getwelcome(self):
58 if self.debugging: print '*welcome*', `self.welcome`
59 return self.welcome
60
61 # Set the debugging level. Argument level means:
62 # 0: no debugging output (default)
63 # 1: print commands and responses but not body text etc.
64 # 2: also print raw lines read and sent before stripping CR/LF
65 def debug(self, level):
66 self.debugging = level
67
68 # Internal: send one line to the server, appending CRLF
69 def putline(self, line):
70 line = line + CRLF
71 if self.debugging > 1: print '*put*', `line`
72 self.sock.send(line)
73
74 # Internal: send one command to the server (through putline())
75 def putcmd(self, line):
76 if self.debugging: print '*cmd*', `line`
77 self.putline(line)
78
79 # Internal: return one line from the server, stripping CRLF.
80 # Raise EOFError if the connection is closed
81 def getline(self):
82 line = self.file.readline()
83 if self.debugging > 1:
84 print '*get*', `line`
85 if not line: raise EOFError
86 if line[-2:] == CRLF: line = line[:-2]
87 elif line[-1:] in CRLF: line = line[:-1]
88 return line
89
90 # Internal: get a response from the server, which may possibly
91 # consist of multiple lines. Return a single string with no
92 # trailing CRLF. If the response consists of multiple lines,
93 # these are separated by '\n' characters in the string
94 def getmultiline(self):
95 line = self.getline()
96 if line[3:4] == '-':
97 code = line[:3]
98 while 1:
99 nextline = self.getline()
100 line = line + ('\n' + nextline)
101 if nextline[:3] == code and \
102 nextline[3:4] <> '-':
103 break
104 return line
105
106 # Internal: get a response from the server.
107 # Raise various errors if the response indicates an error
108 def getresp(self):
109 resp = self.getmultiline()
110 if self.debugging: print '*resp*', `resp`
111 self.lastresp = resp[:3]
112 c = resp[:1]
113 if c == '4':
114 raise error_function, resp
115 if c == '5':
116 raise error_form, resp
117 if c not in '123':
118 raise error_protocol, resp
119 return resp
120
121 # Send a command and return the response
122 def sendcmd(self, cmd):
123 self.putcmd(cmd)
124 return self.getresp()
125
126 # Send a PORT command with the current host and the given port number
127 def sendport(self, port):
128 hostname = socket.gethostname()
129 hostaddr = socket.gethostbyname(hostname)
130 hbytes = string.splitfields(hostaddr, '.')
131 pbytes = [`port/256`, `port%256`]
132 bytes = hbytes + pbytes
133 cmd = 'PORT ' + string.joinfields(bytes, ',')
134 resp = self.sendcmd(cmd)
135 if resp[:3] <> '200':
136 raise error_reply, resp
137
138 # Create a new socket and send a PORT command for it
139 def makeport(self):
140 global nextport
141 port = nextport + PORT_OFFSET
142 nextport = (nextport + 1) % PORT_CYCLE
143 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
144 sock.bind('', port)
145 sock.listen(0)
146 resp = self.sendport(port)
147 return sock
148
149 # Retrieve data in binary mode. (You must set the mode first.)
150 # The argument is a RETR command.
151 # The callback function is called for each block.
152 # This creates a new port for you
153 def retrbinary(self, cmd, callback, blocksize):
154 sock = self.makeport()
155 resp = self.sendcmd(cmd)
156 if resp[0] <> '1':
157 raise error_reply, resp
158 conn, host = sock.accept()
159 sock.close()
160 while 1:
161 data = conn.recv(blocksize)
162 if not data:
163 break
164 callback(data)
165 conn.close()
166 resp = self.getresp()
167 if resp[0] <> '2':
168 raise error_reply, resp
169
170 # Retrieve data in line mode. (You must set the mode first.)
171 # The argument is a RETR or LIST command.
172 # The callback function is called for each line, with trailing
173 # CRLF stripped. This creates a new port for you
174 def retrlines(self, cmd, callback):
175 sock = self.makeport()
176 resp = self.sendcmd(cmd)
177 if resp[0] <> '1':
178 raise error_reply, resp
179 conn, host = sock.accept()
180 sock.close()
181 fp = conn.makefile('r')
182 while 1:
183 line = fp.readline()
184 if not line:
185 break
186 if line[-2:] == CRLF:
187 line = line[:-2]
188 elif line[:-1] == '\n':
189 line = line[:-1]
190 callback(line)
191 fp.close()
192 conn.close()
193 resp = self.getresp()
194 if resp[0] <> '2':
195 raise error_reply, resp
196
197 # Login as user anonymous with given passwd (default user@thishost)
198 def anonymouslogin(self, *args):
199 resp = self.sendcmd('USER anonymous')
200 if resp[0] == '3':
201 if args:
202 passwd = args[0]
203 else:
204 thishost = socket.gethostname()
205 if os.environ.has_key('LOGNAME'):
206 user = os.environ['LOGNAME']
207 elif os.environ.has_key('USER'):
208 user = os.environ['USER']
209 else:
210 user = 'anonymous'
211 passwd = user + '@' + thishost
212 resp = self.sendcmd('PASS ' + passwd)
213 if resp[0] <> '2':
214 raise error_reply, resp
215
216 # Quit, and close the connection
217 def quit(self):
218 resp = self.sendcmd('QUIT')
219 if resp[0] <> '2':
220 raise error_reply, resp
221 self.file.close()
222 self.sock.close()
223
224
225# Test program.
226# Usage: ftp [-d] host [-l[dir]] [-d[dir]] [file] ...
227def test():
228 import marshal
229 global nextport
230 try:
231 nextport = marshal.load(open('.@nextport', 'r'))
232 except IOError:
233 pass
234 try:
235 debugging = 0
236 while sys.argv[1] == '-d':
237 debugging = debugging+1
238 del sys.argv[1]
239 host = sys.argv[1]
240 ftp = FTP().init(host)
241 ftp.debug(debugging)
242 ftp.anonymouslogin()
243 def writeln(line): print line
244 for file in sys.argv[2:]:
245 if file[:2] == '-l':
246 cmd = 'LIST'
247 if file[2:]: cmd = cmd + ' ' + file[2:]
248 ftp.retrlines(cmd, writeln)
249 elif file[:2] == '-d':
250 cmd = 'CWD'
251 if file[2:]: cmd = cmd + ' ' + file[2:]
252 resp = ftp.sendcmd(cmd)
253 else:
254 ftp.retrbinary('RETR ' + file, \
255 sys.stdout.write, 1024)
256 ftp.quit()
257 finally:
258 marshal.dump(nextport, open('.@nextport', 'w'))