aboutsummaryrefslogtreecommitdiff
path: root/src/ftp.lua
diff options
context:
space:
mode:
authorDiego Nehab <diego@tecgraf.puc-rio.br>2004-05-25 05:27:44 +0000
committerDiego Nehab <diego@tecgraf.puc-rio.br>2004-05-25 05:27:44 +0000
commit888496aa821cd09d925046250ea98b1512293fd5 (patch)
tree217e2b532762a64b58c4fffcb2cba08ba2243462 /src/ftp.lua
parent4fc164b8eaa0453050a0a859321c327bb2c4f776 (diff)
downloadluasocket-888496aa821cd09d925046250ea98b1512293fd5.tar.gz
luasocket-888496aa821cd09d925046250ea98b1512293fd5.tar.bz2
luasocket-888496aa821cd09d925046250ea98b1512293fd5.zip
FTP low-level working.
SMTP connection oriented working. ltn12 improved.
Diffstat (limited to '')
-rw-r--r--src/ftp.lua557
1 files changed, 118 insertions, 439 deletions
diff --git a/src/ftp.lua b/src/ftp.lua
index 18dab6d..6074623 100644
--- a/src/ftp.lua
+++ b/src/ftp.lua
@@ -30,474 +30,153 @@ EMAIL = "anonymous@anonymous.org"
30BLOCKSIZE = 2048 30BLOCKSIZE = 2048
31 31
32----------------------------------------------------------------------------- 32-----------------------------------------------------------------------------
33-- Gets ip and port for data connection from PASV answer 33-- Low level FTP API
34-- Input
35-- pasv: PASV command answer
36-- Returns
37-- ip: string containing ip for data connection
38-- port: port for data connection
39----------------------------------------------------------------------------- 34-----------------------------------------------------------------------------
40local function get_pasv(pasv) 35local metat = { __index = {} }
41 local a, b, c, d, p1, p2, _
42 local ip, port
43 _,_, a, b, c, d, p1, p2 =
44 string.find(pasv, "(%d*),(%d*),(%d*),(%d*),(%d*),(%d*)")
45 if not (a and b and c and d and p1 and p2) then return nil, nil end
46 ip = string.format("%d.%d.%d.%d", a, b, c, d)
47 port = tonumber(p1)*256 + tonumber(p2)
48 return ip, port
49end
50
51-----------------------------------------------------------------------------
52-- Check server greeting
53-- Input
54-- control: control connection with server
55-- Returns
56-- code: nil if error
57-- answer: server answer or error message
58-----------------------------------------------------------------------------
59local function greet(control)
60 local code, answer = check_answer(control, {120, 220})
61 if code == 120 then -- please try again, somewhat busy now...
62 return check_answer(control, {220})
63 end
64 return code, answer
65end
66
67-----------------------------------------------------------------------------
68-- Log in on server
69-- Input
70-- control: control connection with server
71-- user: user name
72-- password: user password if any
73-- Returns
74-- code: nil if error
75-- answer: server answer or error message
76-----------------------------------------------------------------------------
77local function login(control, user, password)
78 local code, answer = command(control, "user", user, {230, 331})
79 if code == 331 and password then -- need pass and we have pass
80 return command(control, "pass", password, {230, 202})
81 end
82 return code, answer
83end
84
85-----------------------------------------------------------------------------
86-- Change to target directory
87-- Input
88-- control: socket for control connection with server
89-- path: directory to change to
90-- Returns
91-- code: nil if error
92-- answer: server answer or error message
93-----------------------------------------------------------------------------
94local function cwd(control, path)
95end
96
97-----------------------------------------------------------------------------
98-- Change to target directory
99-- Input
100-- control: socket for control connection with server
101-- Returns
102-- server: server socket bound to local address, nil if error
103-- answer: error message if any
104-----------------------------------------------------------------------------
105local function port(control)
106 local code, answer
107 local server, ctl_ip
108 ctl_ip, answer = control:getsockname()
109 server, answer = socket.bind(ctl_ip, 0)
110 server:settimeout(TIMEOUT)
111 local ip, p, ph, pl
112 ip, p = server:getsockname()
113 pl = math.mod(p, 256)
114 ph = (p - pl)/256
115 local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",")
116 code, answer = command(control, "port", arg, {200})
117 if not code then
118 server:close()
119 return nil, answer
120 else return server end
121end
122 36
123----------------------------------------------------------------------------- 37function open(server, port)
124-- Closes control connection with server 38 local tp = socket.try(socket.tp.connect(server, port or PORT))
125-- Input 39 return setmetatable({tp = tp}, metat)
126-- control: control connection with server
127-- Returns
128-- code: nil if error
129-- answer: server answer or error message
130-----------------------------------------------------------------------------
131local function logout(control)
132 local code, answer = command(control, "quit", nil, {221})
133 if code then control:close() end
134 return code, answer
135end 40end
136 41
137----------------------------------------------------------------------------- 42local function port(portt)
138-- Receives data and send it to a callback 43 return portt.server:accept()
139-- Input
140-- data: data connection
141-- callback: callback to return file contents
142-- Returns
143-- nil if successfull, or an error message in case of error
144-----------------------------------------------------------------------------
145local function receive_indirect(data, callback)
146 local chunk, err, res
147 while not err do
148 chunk, err = try_receive(data, BLOCKSIZE)
149 if err == "closed" then err = "done" end
150 res = callback(chunk, err)
151 if not res then break end
152 end
153end 44end
154 45
155----------------------------------------------------------------------------- 46local function pasv(pasvt)
156-- Retrieves file or directory listing 47 return socket.connect(pasvt.ip, pasvt.port)
157-- Input
158-- control: control connection with server
159-- server: server socket bound to local address
160-- name: file name
161-- is_directory: is file a directory name?
162-- content_cb: callback to receive file contents
163-- Returns
164-- err: error message in case of error, nil otherwise
165-----------------------------------------------------------------------------
166local function retrieve(control, server, name, is_directory, content_cb)
167 local code, answer
168 local data
169 -- ask server for file or directory listing accordingly
170 if is_directory then
171 code, answer = cwd(control, name)
172 if not code then return answer end
173 code, answer = command(control, "nlst", nil, {150, 125})
174 else
175 code, answer = command(control, "retr", name, {150, 125})
176 end
177 if not code then return nil, answer end
178 data, answer = server:accept()
179 server:close()
180 if not data then
181 control:close()
182 return answer
183 end
184 answer = receive_indirect(data, content_cb)
185 if answer then
186 control:close()
187 return answer
188 end
189 data:close()
190 -- make sure file transfered ok
191 return check_answer(control, {226, 250})
192end 48end
193 49
194----------------------------------------------------------------------------- 50function metat.__index:login(user, password)
195-- Stores a file 51 socket.try(self.tp:command("USER", user))
196-- Input 52 local code, reply = socket.try(self.tp:check{"2..", 331})
197-- control: control connection with server 53 if code == 331 then
198-- server: server socket bound to local address 54 socket.try(password, reply)
199-- file: file name under current directory 55 socket.try(self.tp:command("PASS", password))
200-- send_cb: callback to produce the file contents 56 socket.try(self.tp:check("2.."))
201-- Returns 57 end
202-- code: return code, nil if error 58 return 1
203-- answer: server answer or error message 59end
204----------------------------------------------------------------------------- 60
205local function store(control, server, file, send_cb) 61function metat.__index:pasv()
206 local data, err 62 socket.try(self.tp:command("PASV"))
207 local code, answer = command(control, "stor", file, {150, 125}) 63 local code, reply = socket.try(self.tp:check("2.."))
208 if not code then 64 local _, _, a, b, c, d, p1, p2 =
209 control:close() 65 string.find(reply, "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)")
210 return nil, answer 66 socket.try(a and b and c and d and p1 and p2, reply)
211 end 67 self.pasvt = {
212 -- start data connection 68 ip = string.format("%d.%d.%d.%d", a, b, c, d),
213 data, answer = server:accept() 69 port = p1*256 + p2
214 server:close() 70 }
215 if not data then 71 if self.portt then
216 control:close() 72 self.portt.server:close()
217 return nil, answer 73 self.portt = nil
218 end 74 end
219 -- send whole file 75 return self.pasvt.ip, self.pasvt.port
220 err = send_indirect(data, send_cb, send_cb())
221 if err then
222 control:close()
223 return nil, err
224 end
225 -- close connection to inform that file transmission is complete
226 data:close()
227 -- check if file was received correctly
228 return check_answer(control, {226, 250})
229end
230
231-----------------------------------------------------------------------------
232-- Change transfer type
233-- Input
234-- control: control connection with server
235-- params: "type=i" for binary or "type=a" for ascii
236-- Returns
237-- err: error message if any
238-----------------------------------------------------------------------------
239local function change_type(control, params)
240 local type, _
241 _, _, type = string.find(params or "", "type=(.)")
242 if type == "a" or type == "i" then
243 local code, err = command(control, "type", type, {200})
244 if not code then return err end
245 end
246end 76end
247 77
248----------------------------------------------------------------------------- 78function metat.__index:port(ip, port)
249-- Starts a control connection, checks the greeting and log on 79 self.pasvt = nil
250-- Input 80 local server
251-- parsed: parsed URL components 81 if not ip then
252-- Returns 82 ip, port = socket.try(self.tp:getcontrol():getsockname())
253-- control: control connection with server, or nil if error 83 server = socket.try(socket.bind(ip, 0))
254-- err: error message if any 84 ip, port = socket.try(server:getsockname())
255----------------------------------------------------------------------------- 85 socket.try(server:settimeout(TIMEOUT))
256local function open(parsed) 86 end
257 local control, err = socket.tp.connect(parsed.host, parsed.port) 87 local pl = math.mod(port, 256)
258 if not control then return nil, err end 88 local ph = (port - pl)/256
89 local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",")
90 socket.try(self.tp:command("port", arg))
91 socket.try(self.tp:check("2.."))
92 self.portt = server and {ip = ip, port = port, server = server}
93 return 1
94end
95
96function metat.__index:send(sendt)
97 local data
98 socket.try(self.pasvt or self.portt, "need port or pasv first")
99 if self.pasvt then data = socket.try(pasv(self.pasvt)) end
100 socket.try(self.tp:command(sendt.command, sendt.argument))
101 if self.portt then data = socket.try(port(self.portt)) end
102 local step = sendt.step or ltn12.pump.step
259 local code, reply 103 local code, reply
260 -- greet 104 local checkstep = function(src, snk)
261 code, reply = control:check({120, 220}) 105 local readyt = socket.select(readt, nil, 0)
262 if code == 120 then -- busy, try again 106 if readyt[tp] then
263 code, reply = control:check(220) 107 code, reply = self.tp:check{"2..", "1.."}
108 if not code then
109 data:close()
110 return nil, reply
111 end
112 end
113 local ret, err = step(src, snk)
114 if err then data:close() end
115 return ret, err
264 end 116 end
265 -- authenticate 117 local sink = socket.sink("close-when-empty", data)
266 code, reply = control:command("user", user) 118 socket.try(ltn12.pump.all(sendt.source, sink, checkstep))
267 code, reply = control:check({230, 331}) 119 if not code then code = socket.try(self.tp:check{"1..", "2.."}) end
268 if code == 331 and password then -- need pass and we have pass 120 if string.find(code, "1..") then socket.try(self.tp:check("2..")) end
269 control:command("pass", password) 121 return 1
270 code, reply = control:check({230, 202}) 122end
123
124function metat.__index:receive(recvt)
125 local data
126 socket.try(self.pasvt or self.portt, "need port or pasv first")
127 if self.pasvt then data = socket.try(pasv(self.pasvt)) end
128 socket.try(self.tp:command(recvt.command, recvt.argument))
129 if self.portt then data = socket.try(port(self.portt)) end
130 local source = socket.source("until-closed", data)
131 local step = recvt.step or ltn12.pump.step
132 local checkstep = function(src, snk)
133 local ret, err = step(src, snk)
134 if err then data:close() end
135 return ret, err
271 end 136 end
272 -- change directory 137 socket.try(ltn12.pump.all(source, recvt.sink, checkstep))
273 local segment = parse_path(parsed) 138 local code = socket.try(self.tp:check{"1..", "2.."})
274 for i, v in ipairs(segment) do 139 if string.find(code, "1..") then socket.try(self.tp:check("2..")) end
275 code, reply = control:command("cwd") 140 return 1
276 code, reply = control:check(250)
277 end
278 -- change type
279 local type = string.sub(params or "", 7, 7)
280 if type == "a" or type == "i" then
281 code, reply = control:command("type", type)
282 code, reply = control:check(200)
283 end
284end 141end
285 142
286 return change_dir(control, segment) or 143function metat.__index:cwd(dir)
287 change_type(control, parsed.params) or 144 socket.try(self.tp:command("CWD", dir))
288 download(control, request, segment) or 145 socket.try(self.tp:check(250))
289 close(control) 146 return 1
290end 147end
291 148
149function metat.__index:type(type)
150 socket.try(self.tp:command("TYPE", type))
151 socket.try(self.tp:check(200))
152 return 1
292end 153end
293 154
294----------------------------------------------------------------------------- 155function metat.__index:greet()
295-- Stores a file in current directory 156 local code = socket.try(self.tp:check{"1..", "2.."})
296-- Input 157 if string.find(code, "1..") then socket.try(self.tp:check("2..")) end
297-- control: control connection with server 158 return 1
298-- request: a table with the fields:
299-- content_cb: send callback to send file contents
300-- segment: parsed URL path segments
301-- Returns
302-- err: error message if any
303-----------------------------------------------------------------------------
304local function upload(control, request, segment)
305 local code, name, content_cb
306 -- get remote file name
307 name = segment[table.getn(segment)]
308 if not name then
309 control:close()
310 return "Invalid file path"
311 end
312 content_cb = request.content_cb
313 -- setup passive connection
314 local server, answer = port(control)
315 if not server then return answer end
316 -- ask server to receive file
317 code, answer = store(control, server, name, content_cb)
318 if not code then return answer end
319end 159end
320 160
321----------------------------------------------------------------------------- 161function metat.__index:quit()
322-- Download a file from current directory 162 socket.try(self.tp:command("QUIT"))
323-- Input 163 socket.try(self.tp:check("2.."))
324-- control: control connection with server 164 return 1
325-- request: a table with the fields:
326-- content_cb: receive callback to receive file contents
327-- segment: parsed URL path segments
328-- Returns
329-- err: error message if any
330-----------------------------------------------------------------------------
331local function download(control, request, segment)
332 local code, name, is_directory, content_cb
333 is_directory = segment.is_directory
334 content_cb = request.content_cb
335 -- get remote file name
336 name = segment[table.getn(segment)]
337 if not name and not is_directory then
338 control:close()
339 return "Invalid file path"
340 end
341 -- setup passive connection
342 local server, answer = port(control)
343 if not server then return answer end
344 -- ask server to send file or directory listing
345 code, answer = retrieve(control, server, name,
346 is_directory, content_cb)
347 if not code then return answer end
348end 165end
349 166
350----------------------------------------------------------------------------- 167function metat.__index:close()
351-- Parses the FTP URL setting default values 168 socket.try(self.tp:close())
352-- Input 169 return 1
353-- request: a table with the fields:
354-- url: the target URL
355-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
356-- user: account user name
357-- password: account password
358-- Returns
359-- parsed: a table with parsed components
360-----------------------------------------------------------------------------
361local function parse_url(request)
362 local parsed = socket.url.parse(request.url, {
363 user = "anonymous",
364 port = 21,
365 path = "/",
366 password = EMAIL,
367 scheme = "ftp"
368 })
369 -- explicit login information overrides that given by URL
370 parsed.user = request.user or parsed.user
371 parsed.password = request.password or parsed.password
372 -- explicit representation type overrides that given by URL
373 if request.type then parsed.params = "type=" .. request.type end
374 return parsed
375end 170end
376 171
377----------------------------------------------------------------------------- 172-----------------------------------------------------------------------------
378-- Parses the FTP URL path setting default values 173-- High level FTP API
379-- Input
380-- parsed: a table with the parsed URL components
381-- Returns
382-- dirs: a table with parsed directory components
383----------------------------------------------------------------------------- 174-----------------------------------------------------------------------------
384local function parse_path(parsed_url)
385 local segment = socket.url.parse_path(parsed_url.path)
386 segment.is_directory = segment.is_directory or
387 (parsed_url.params == "type=d")
388 return segment
389end
390 175
391----------------------------------------------------------------------------- 176function put(putt)
392-- Builds a request table from a URL or request table
393-- Input
394-- url_or_request: target url or request table (a table with the fields:
395-- url: the target URL
396-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
397-- user: account user name
398-- password: account password)
399-- Returns
400-- request: request table
401-----------------------------------------------------------------------------
402local function build_request(data)
403 local request = {}
404 if type(data) == "table" then for i, v in data do request[i] = v end
405 else request.url = data end
406 return request
407end 177end
408 178
409----------------------------------------------------------------------------- 179function get(gett)
410-- Downloads a file from a FTP server
411-- Input
412-- request: a table with the fields:
413-- url: the target URL
414-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
415-- user: account user name
416-- password: account password
417-- content_cb: receive callback to receive file contents
418-- Returns
419-- err: error message if any
420-----------------------------------------------------------------------------
421function get_cb(request)
422 local parsed = parse_url(request)
423 if parsed.scheme ~= "ftp" then
424 return string.format("unknown scheme '%s'", parsed.scheme)
425 end
426 local control, err = open(parsed)
427 if not control then return err end
428 local segment = parse_path(parsed)
429 return change_dir(control, segment) or
430 change_type(control, parsed.params) or
431 download(control, request, segment) or
432 close(control)
433end
434
435-----------------------------------------------------------------------------
436-- Uploads a file to a FTP server
437-- Input
438-- request: a table with the fields:
439-- url: the target URL
440-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
441-- user: account user name
442-- password: account password
443-- content_cb: send callback to send file contents
444-- Returns
445-- err: error message if any
446-----------------------------------------------------------------------------
447function put_cb(request)
448 local parsed = parse_url(request)
449 if parsed.scheme ~= "ftp" then
450 return string.format("unknown scheme '%s'", parsed.scheme)
451 end
452 local control, err = open(parsed)
453 if not control then return err end
454 local segment = parse_path(parsed)
455 err = change_dir(control, segment) or
456 change_type(control, parsed.params) or
457 upload(control, request, segment) or
458 close(control)
459 if err then return nil, err
460 else return 1 end
461end
462
463-----------------------------------------------------------------------------
464-- Uploads a file to a FTP server
465-- Input
466-- url_or_request: target url or request table (a table with the fields:
467-- url: the target URL
468-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
469-- user: account user name
470-- password: account password)
471-- content: file contents
472-- content: file contents
473-- Returns
474-- err: error message if any
475-----------------------------------------------------------------------------
476function put(url_or_request, content)
477 local request = build_request(url_or_request)
478 request.content = request.content or content
479 request.content_cb = socket.callback.send_string(request.content)
480 return put_cb(request)
481end
482
483-----------------------------------------------------------------------------
484-- Retrieve a file from a ftp server
485-- Input
486-- url_or_request: target url or request table (a table with the fields:
487-- url: the target URL
488-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
489-- user: account user name
490-- password: account password)
491-- Returns
492-- data: file contents as a string
493-- err: error message in case of error, nil otherwise
494-----------------------------------------------------------------------------
495function get(url_or_request)
496 local concat = socket.concat.create()
497 local request = build_request(url_or_request)
498 request.content_cb = socket.callback.receive_concat(concat)
499 local err = get_cb(request)
500 return concat:getresult(), err
501end 180end
502 181
503return socket.ftp 182return ftp