diff options
Diffstat (limited to 'src/ftp.lua')
-rw-r--r-- | src/ftp.lua | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/src/ftp.lua b/src/ftp.lua new file mode 100644 index 0000000..b817356 --- /dev/null +++ b/src/ftp.lua | |||
@@ -0,0 +1,437 @@ | |||
1 | ----------------------------------------------------------------------------- | ||
2 | -- Simple FTP support for the Lua language using the LuaSocket toolkit. | ||
3 | -- Author: Diego Nehab | ||
4 | -- Date: 26/12/2000 | ||
5 | -- Conforming to: RFC 959 | ||
6 | ----------------------------------------------------------------------------- | ||
7 | |||
8 | ----------------------------------------------------------------------------- | ||
9 | -- Program constants | ||
10 | ----------------------------------------------------------------------------- | ||
11 | -- timeout in seconds before the program gives up on a connection | ||
12 | local TIMEOUT = 60 | ||
13 | -- default port for ftp service | ||
14 | local PORT = 21 | ||
15 | -- this is the default anonymous password. used when no password is | ||
16 | -- provided in url. should be changed for your e-mail. | ||
17 | local EMAIL = "anonymous@anonymous.org" | ||
18 | |||
19 | ----------------------------------------------------------------------------- | ||
20 | -- Parses a url and returns its scheme, user, password, host, port | ||
21 | -- and path components, according to RFC 1738, Uniform Resource Locators (URL), | ||
22 | -- of December 1994 | ||
23 | -- Input | ||
24 | -- url: unique resource locator desired | ||
25 | -- default: table containing default values to be returned | ||
26 | -- Returns | ||
27 | -- table with the following fields: | ||
28 | -- host: host to connect | ||
29 | -- path: url path | ||
30 | -- port: host port to connect | ||
31 | -- user: user name | ||
32 | -- pass: password | ||
33 | -- scheme: protocol | ||
34 | ----------------------------------------------------------------------------- | ||
35 | local split_url = function(url, default) | ||
36 | -- initialize default parameters | ||
37 | local parsed = default or {} | ||
38 | -- get scheme | ||
39 | url = gsub(url, "^(.+)://", function (s) %parsed.scheme = s end) | ||
40 | -- get user name and password. both can be empty! | ||
41 | -- moreover, password can be ommited | ||
42 | url = gsub(url, "^([^@:/]*)(:?)([^:@/]-)@", function (u, c, p) | ||
43 | %parsed.user = u | ||
44 | -- there can be an empty password, but the ':' has to be there | ||
45 | -- or else there is no password | ||
46 | %parsed.pass = nil -- kill default password | ||
47 | if c == ":" then %parsed.pass = p end | ||
48 | end) | ||
49 | -- get host | ||
50 | url = gsub(url, "^([%w%.%-]+)", function (h) %parsed.host = h end) | ||
51 | -- get port if any | ||
52 | url = gsub(url, "^:(%d+)", function (p) %parsed.port = p end) | ||
53 | -- whatever is left is the path | ||
54 | if url ~= "" then parsed.path = url end | ||
55 | return parsed | ||
56 | end | ||
57 | |||
58 | ----------------------------------------------------------------------------- | ||
59 | -- Gets ip and port for data connection from PASV answer | ||
60 | -- Input | ||
61 | -- pasv: PASV command answer | ||
62 | -- Returns | ||
63 | -- ip: string containing ip for data connection | ||
64 | -- port: port for data connection | ||
65 | ----------------------------------------------------------------------------- | ||
66 | local get_pasv = function(pasv) | ||
67 | local a,b,c,d,p1,p2 | ||
68 | local ip, port | ||
69 | _,_, a, b, c, d, p1, p2 = | ||
70 | strfind(pasv, "(%d*),(%d*),(%d*),(%d*),(%d*),(%d*)") | ||
71 | if not a or not b or not c or not d or not p1 or not p2 then | ||
72 | return nil, nil | ||
73 | end | ||
74 | ip = format("%d.%d.%d.%d", a, b, c, d) | ||
75 | port = tonumber(p1)*256 + tonumber(p2) | ||
76 | return ip, port | ||
77 | end | ||
78 | |||
79 | ----------------------------------------------------------------------------- | ||
80 | -- Sends a FTP command through socket | ||
81 | -- Input | ||
82 | -- control: control connection socket | ||
83 | -- cmd: command | ||
84 | -- arg: command argument if any | ||
85 | ----------------------------------------------------------------------------- | ||
86 | local send_command = function(control, cmd, arg) | ||
87 | local line, err | ||
88 | if arg then line = cmd .. " " .. arg .. "\r\n" | ||
89 | else line = cmd .. "\r\n" end | ||
90 | err = control:send(line) | ||
91 | return err | ||
92 | end | ||
93 | |||
94 | ----------------------------------------------------------------------------- | ||
95 | -- Gets FTP command answer, unfolding if neccessary | ||
96 | -- Input | ||
97 | -- control: control connection socket | ||
98 | -- Returns | ||
99 | -- answer: whole server reply, nil if error | ||
100 | -- code: answer status code or error message | ||
101 | ----------------------------------------------------------------------------- | ||
102 | local get_answer = function(control) | ||
103 | local code, lastcode, sep | ||
104 | local line, err = control:receive() | ||
105 | local answer = line | ||
106 | if err then return nil, err end | ||
107 | _,_, code, sep = strfind(line, "^(%d%d%d)(.)") | ||
108 | if not code or not sep then return nil, answer end | ||
109 | if sep == "-" then -- answer is multiline | ||
110 | repeat | ||
111 | line, err = control:receive() | ||
112 | if err then return nil, err end | ||
113 | _,_, lastcode, sep = strfind(line, "^(%d%d%d)(.)") | ||
114 | answer = answer .. "\n" .. line | ||
115 | until code == lastcode and sep == " " -- answer ends with same code | ||
116 | end | ||
117 | return answer, tonumber(code) | ||
118 | end | ||
119 | |||
120 | ----------------------------------------------------------------------------- | ||
121 | -- Checks if a message return is correct. Closes control connection if not. | ||
122 | -- Input | ||
123 | -- control: control connection socket | ||
124 | -- success: table with successfull reply status code | ||
125 | -- Returns | ||
126 | -- code: reply code or nil in case of error | ||
127 | -- answer: server complete answer or system error message | ||
128 | ----------------------------------------------------------------------------- | ||
129 | local check_answer = function(control, success) | ||
130 | local answer, code = %get_answer(control) | ||
131 | if not answer then | ||
132 | control:close() | ||
133 | return nil, code | ||
134 | end | ||
135 | if type(success) ~= "table" then success = {success} end | ||
136 | for i = 1, getn(success) do | ||
137 | if code == success[i] then | ||
138 | return code, answer | ||
139 | end | ||
140 | end | ||
141 | control:close() | ||
142 | return nil, answer | ||
143 | end | ||
144 | |||
145 | ----------------------------------------------------------------------------- | ||
146 | -- Trys a command on control socked, in case of error, the control connection | ||
147 | -- is closed. | ||
148 | -- Input | ||
149 | -- control: control connection socket | ||
150 | -- cmd: command | ||
151 | -- arg: command argument or nil if no argument | ||
152 | -- success: table with successfull reply status code | ||
153 | -- Returns | ||
154 | -- code: reply code or nil in case of error | ||
155 | -- answer: server complete answer or system error message | ||
156 | ----------------------------------------------------------------------------- | ||
157 | local try_command = function(control, cmd, arg, success) | ||
158 | local err = %send_command(control, cmd, arg) | ||
159 | if err then | ||
160 | control:close() | ||
161 | return nil, err | ||
162 | end | ||
163 | local code, answer = %check_answer(control, success) | ||
164 | if not code then return nil, answer end | ||
165 | return code, answer | ||
166 | end | ||
167 | |||
168 | ----------------------------------------------------------------------------- | ||
169 | -- Creates a table with all directories in path | ||
170 | -- Input | ||
171 | -- file: abolute path to file | ||
172 | -- Returns | ||
173 | -- file: filename | ||
174 | -- path: table with directories to reach filename | ||
175 | -- isdir: is it a directory or a file | ||
176 | ----------------------------------------------------------------------------- | ||
177 | local split_path = function(file) | ||
178 | local path = {} | ||
179 | local isdir | ||
180 | file = file or "/" | ||
181 | -- directory ends with a '/' | ||
182 | _,_, isdir = strfind(file, "([/])$") | ||
183 | gsub(file, "([^/]+)", function (dir) tinsert(%path, dir) end) | ||
184 | if not isdir then file = tremove(path) | ||
185 | else file = nil end | ||
186 | return file, path, isdir | ||
187 | end | ||
188 | |||
189 | ----------------------------------------------------------------------------- | ||
190 | -- Check server greeting | ||
191 | -- Input | ||
192 | -- control: control connection with server | ||
193 | -- Returns | ||
194 | -- code: nil if error | ||
195 | -- answer: server answer or error message | ||
196 | ----------------------------------------------------------------------------- | ||
197 | local check_greeting = function(control) | ||
198 | local code, answer = %check_answer(control, {120, 220}) | ||
199 | if not code then return nil, answer end | ||
200 | if code == 120 then -- please try again, somewhat busy now... | ||
201 | code, answer = %check_answer(control, {220}) | ||
202 | end | ||
203 | return code, answer | ||
204 | end | ||
205 | |||
206 | ----------------------------------------------------------------------------- | ||
207 | -- Log in on server | ||
208 | -- Input | ||
209 | -- control: control connection with server | ||
210 | -- user: user name | ||
211 | -- pass: user password if any | ||
212 | -- Returns | ||
213 | -- code: nil if error | ||
214 | -- answer: server answer or error message | ||
215 | ----------------------------------------------------------------------------- | ||
216 | local login = function(control, user, pass) | ||
217 | local code, answer = %try_command(control, "user", parsed.user, {230, 331}) | ||
218 | if not code then return nil, answer end | ||
219 | if code == 331 and parsed.pass then -- need pass and we have pass | ||
220 | code, answer = %try_command(control, "pass", parsed.pass, {230, 202}) | ||
221 | end | ||
222 | return code, answer | ||
223 | end | ||
224 | |||
225 | ----------------------------------------------------------------------------- | ||
226 | -- Change to target directory | ||
227 | -- Input | ||
228 | -- control: socket for control connection with server | ||
229 | -- path: array with directories in order | ||
230 | -- Returns | ||
231 | -- code: nil if error | ||
232 | -- answer: server answer or error message | ||
233 | ----------------------------------------------------------------------------- | ||
234 | local cwd = function(control, path) | ||
235 | local code, answer = 250, "Home directory used" | ||
236 | for i = 1, getn(path) do | ||
237 | code, answer = %try_command(control, "cwd", path[i], {250}) | ||
238 | if not code then return nil, answer end | ||
239 | end | ||
240 | return code, answer | ||
241 | end | ||
242 | |||
243 | ----------------------------------------------------------------------------- | ||
244 | -- Start data connection with server | ||
245 | -- Input | ||
246 | -- control: control connection with server | ||
247 | -- Returns | ||
248 | -- data: socket for data connection with server, nil if error | ||
249 | -- answer: server answer or error message | ||
250 | ----------------------------------------------------------------------------- | ||
251 | local start_dataconnection = function(control) | ||
252 | -- ask for passive data connection | ||
253 | local code, answer = %try_command(control, "pasv", nil, {227}) | ||
254 | if not code then return nil, answer end | ||
255 | -- get data connection parameters from server reply | ||
256 | local host, port = %get_pasv(answer) | ||
257 | if not host or not port then return nil, answer end | ||
258 | -- start data connection with given parameters | ||
259 | local data, err = connect(host, port) | ||
260 | if not data then return nil, err end | ||
261 | data:timeout(%TIMEOUT) | ||
262 | return data | ||
263 | end | ||
264 | |||
265 | ----------------------------------------------------------------------------- | ||
266 | -- Closes control connection with server | ||
267 | -- Input | ||
268 | -- control: control connection with server | ||
269 | -- Returns | ||
270 | -- code: nil if error | ||
271 | -- answer: server answer or error message | ||
272 | ----------------------------------------------------------------------------- | ||
273 | local logout = function(control) | ||
274 | local code, answer = %try_command(control, "quit", nil, {221}) | ||
275 | if not code then return nil, answer end | ||
276 | control:close() | ||
277 | return code, answer | ||
278 | end | ||
279 | |||
280 | ----------------------------------------------------------------------------- | ||
281 | -- Retrieves file or directory listing | ||
282 | -- Input | ||
283 | -- control: control connection with server | ||
284 | -- data: data connection with server | ||
285 | -- file: file name under current directory | ||
286 | -- isdir: is file a directory name? | ||
287 | -- Returns | ||
288 | -- file: string with file contents, nil if error | ||
289 | -- answer: server answer or error message | ||
290 | ----------------------------------------------------------------------------- | ||
291 | local retrieve_file = function(control, data, file, isdir) | ||
292 | -- ask server for file or directory listing accordingly | ||
293 | if isdir then code, answer = %try_command(control, "nlst", file, {150, 125}) | ||
294 | else code, answer = %try_command(control, "retr", file, {150, 125}) end | ||
295 | if not code then | ||
296 | control:close() | ||
297 | data:close() | ||
298 | return nil, answer | ||
299 | end | ||
300 | -- download whole file | ||
301 | file, err = data:receive("*a") | ||
302 | data:close() | ||
303 | if err then | ||
304 | control:close() | ||
305 | return nil, err | ||
306 | end | ||
307 | -- make sure file transfered ok | ||
308 | code, answer = %check_answer(control, {226, 250}) | ||
309 | if not code then return nil, answer | ||
310 | else return file, answer end | ||
311 | end | ||
312 | |||
313 | ----------------------------------------------------------------------------- | ||
314 | -- Stores a file | ||
315 | -- Input | ||
316 | -- control: control connection with server | ||
317 | -- data: data connection with server | ||
318 | -- file: file name under current directory | ||
319 | -- bytes: file contents in string | ||
320 | -- Returns | ||
321 | -- file: string with file contents, nil if error | ||
322 | -- answer: server answer or error message | ||
323 | ----------------------------------------------------------------------------- | ||
324 | local store_file = function (control, data, file, bytes) | ||
325 | local code, answer = %try_command(control, "stor", file, {150, 125}) | ||
326 | if not code then | ||
327 | data:close() | ||
328 | return nil, answer | ||
329 | end | ||
330 | -- send whole file and close connection to mark file end | ||
331 | answer = data:send(bytes) | ||
332 | data:close() | ||
333 | if answer then | ||
334 | control:close() | ||
335 | return nil, answer | ||
336 | end | ||
337 | -- check if file was received right | ||
338 | return %check_answer(control, {226, 250}) | ||
339 | end | ||
340 | |||
341 | ----------------------------------------------------------------------------- | ||
342 | -- Change transfer type | ||
343 | -- Input | ||
344 | -- control: control connection with server | ||
345 | -- type: new transfer type | ||
346 | -- Returns | ||
347 | -- code: nil if error | ||
348 | -- answer: server answer or error message | ||
349 | ----------------------------------------------------------------------------- | ||
350 | local change_type = function(control, type) | ||
351 | if type == "b" then type = "i" else type = "a" end | ||
352 | return %try_command(control, "type", type, {200}) | ||
353 | end | ||
354 | |||
355 | ----------------------------------------------------------------------------- | ||
356 | -- Retrieve a file from a ftp server | ||
357 | -- Input | ||
358 | -- url: file location | ||
359 | -- type: "binary" or "ascii" | ||
360 | -- Returns | ||
361 | -- file: downloaded file or nil in case of error | ||
362 | -- err: error message if any | ||
363 | ----------------------------------------------------------------------------- | ||
364 | function ftp_get(url, type) | ||
365 | local control, data, err | ||
366 | local answer, code, server, file, path | ||
367 | parsed = %split_url(url, {user = "anonymous", port = 21, pass = %EMAIL}) | ||
368 | -- start control connection | ||
369 | control, err = connect(parsed.host, parsed.port) | ||
370 | if not control then return nil, err end | ||
371 | control:timeout(%TIMEOUT) | ||
372 | -- get and check greeting | ||
373 | code, answer = %check_greeting(control) | ||
374 | if not code then return nil, answer end | ||
375 | -- try to log in | ||
376 | code, answer = %login(control, parsed.user, parsed.pass) | ||
377 | if not code then return nil, answer end | ||
378 | -- go to directory | ||
379 | file, path, isdir = %split_path(parsed.path) | ||
380 | code, answer = %cwd(control, path) | ||
381 | if not code then return nil, answer end | ||
382 | -- change to binary type? | ||
383 | code, answer = %change_type(control, type) | ||
384 | if not code then return nil, answer end | ||
385 | -- start data connection | ||
386 | data, answer = %start_dataconnection(control) | ||
387 | if not data then return nil, answer end | ||
388 | -- ask server to send file or directory listing | ||
389 | file, answer = %retrieve_file(control, data, file, isdir) | ||
390 | if not file then return nil, answer end | ||
391 | -- disconnect | ||
392 | %logout(control) | ||
393 | -- return whatever file we received plus a possible error | ||
394 | return file, answer | ||
395 | end | ||
396 | |||
397 | ----------------------------------------------------------------------------- | ||
398 | -- Uploads a file to a FTP server | ||
399 | -- Input | ||
400 | -- url: file location | ||
401 | -- bytes: file contents | ||
402 | -- type: "binary" or "ascii" | ||
403 | -- Returns | ||
404 | -- err: error message if any | ||
405 | ----------------------------------------------------------------------------- | ||
406 | function ftp_put(url, bytes, type) | ||
407 | local control, data | ||
408 | local answer, code, server, file, path | ||
409 | parsed = %split_url(url, {user = "anonymous", port = 21, pass = %EMAIL}) | ||
410 | -- start control connection | ||
411 | control, answer = connect(parsed.host, parsed.port) | ||
412 | if not control then return answer end | ||
413 | control:timeout(%TIMEOUT) | ||
414 | -- get and check greeting | ||
415 | code, answer = %check_greeting(control) | ||
416 | if not code then return answer end | ||
417 | -- try to log in | ||
418 | code, answer = %login(control, parsed.user, parsed.pass) | ||
419 | if not code then return answer end | ||
420 | -- go to directory | ||
421 | file, path, isdir = %split_path(parsed.path) | ||
422 | code, answer = %cwd(control, path) | ||
423 | if not code then return answer end | ||
424 | -- change to binary type? | ||
425 | code, answer = %change_type(control, type) | ||
426 | if not code then return answer end | ||
427 | -- start data connection | ||
428 | data, answer = %start_dataconnection(control) | ||
429 | if not data then return answer end | ||
430 | -- ask server to send file or directory listing | ||
431 | code, answer = %store_file(control, data, file, bytes) | ||
432 | if not code then return answer end | ||
433 | -- disconnect | ||
434 | %logout(control) | ||
435 | -- return whatever file we received plus a possible error | ||
436 | return nil | ||
437 | end | ||