1
2
3
4local setmetatable, type, next = setmetatable, type, next
5local find, format, gsub, match = string.find, string.format, string.gsub, string.match
6local concat = table.concat
7local mod = math.mod
8
9local socket = socket or require("socket")
10local url = socket.url or require("socket.url")
11local tp = socket.tp or require("socket.tp")
12local ltn12 = ltn12 or require("ltn12")
13
14local tcpsocket = socket.tcp
15local trysocket = socket.try
16local skipsocket = socket.skip
17local sinksocket = socket.sink
18local selectsocket = socket.select
19local bindsocket = socket.bind
20local newtrysocket = socket.newtry
21local sourcesocket = socket.source
22local protectsocket = socket.protect
23
24local parseurl = url.parse
25local unescapeurl = url.unescape
26
27local pumpall = ltn12.pump.all
28local pumpstep = ltn12.pump.step
29local sourcestring = ltn12.source.string
30local sinktable = ltn12.sink.table
31
32local ftp = {
33 TIMEOUT = 60,
34 USER = "ftp",
35 PASSWORD = "anonymous@anonymous.org",
36}
37
38socket.ftp = ftp
39
40local PORT = 21
41
42local methods = { }
43local mt = { __index = methods }
44
45function ftp.open(server, port, create)
46 local tp = trysocket(tp.connect(server, port or PORT, ftp.TIMEOUT, create))
47 local f = setmetatable({ tp = tp }, metat)
48 f.try = newtrysocket(function() f:close() end)
49 return f
50end
51
52function methods.portconnect(self)
53 local try = self.try
54 local server = self.server
55 try(server:settimeout(ftp.TIMEOUT))
56 self.data = try(server:accept())
57 try(self.data:settimeout(ftp.TIMEOUT))
58end
59
60function methods.pasvconnect(self)
61 local try = self.try
62 self.data = try(tcpsocket())
63 self(self.data:settimeout(ftp.TIMEOUT))
64 self(self.data:connect(self.pasvt.address, self.pasvt.port))
65end
66
67function methods.login(self, user, password)
68 local try = self.try
69 local tp = self.tp
70 try(tp:command("user", user or ftp.USER))
71 local code, reply = try(tp:check{"2..", 331})
72 if code == 331 then
73 try(tp:command("pass", password or ftp.PASSWORD))
74 try(tp:check("2.."))
75 end
76 return 1
77end
78
79function methods.pasv(self)
80 local try = self.try
81 local tp = self.tp
82 try(tp:command("pasv"))
83 local code, reply = try(self.tp:check("2.."))
84 local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)"
85 local a, b, c, d, p1, p2 = skipsocket(2, find(reply, pattern))
86 try(a and b and c and d and p1 and p2, reply)
87 local address = format("%d.%d.%d.%d", a, b, c, d)
88 local port = p1*256 + p2
89 local server = self.server
90 self.pasvt = {
91 address = address,
92 port = port,
93 }
94 if server then
95 server:close()
96 self.server = nil
97 end
98 return address, port
99end
100
101function methods.epsv(self)
102 local try = self.try
103 local tp = self.tp
104 try(tp:command("epsv"))
105 local code, reply = try(tp:check("229"))
106 local pattern = "%((.)(.-)%1(.-)%1(.-)%1%)"
107 local d, prt, address, port = match(reply, pattern)
108 try(port, "invalid epsv response")
109 local address = tp:getpeername()
110 local server = self.server
111 self.pasvt = {
112 address = address,
113 port = port,
114 }
115 if self.server then
116 server:close()
117 self.server = nil
118 end
119 return address, port
120end
121
122function methods.port(self, address, port)
123 local try = self.try
124 local tp = self.tp
125 self.pasvt = nil
126 if not address then
127 address, port = try(tp:getsockname())
128 self.server = try(bindsocket(address, 0))
129 address, port = try(self.server:getsockname())
130 try(self.server:settimeout(ftp.TIMEOUT))
131 end
132 local pl = mod(port,256)
133 local ph = (port - pl)/256
134 local arg = gsub(format("%s,%d,%d", address, ph, pl), "%.", ",")
135 try(tp:command("port", arg))
136 try(tp:check("2.."))
137 return 1
138end
139
140function methods.eprt(self, family, address, port)
141 local try = self.try
142 local tp = self.tp
143 self.pasvt = nil
144 if not address then
145 address, port = try(tp:getsockname())
146 self.server = try(bindsocket(address, 0))
147 address, port = try(self.server:getsockname())
148 try(self.server:settimeout(ftp.TIMEOUT))
149 end
150 local arg = format("|%s|%s|%d|", family, address, port)
151 try(tp:command("eprt", arg))
152 try(tp:check("2.."))
153 return 1
154end
155
156function methods.send(self, sendt)
157 local try = self.try
158 local tp = self.tp
159
160 try(self.pasvt or self.server, "need port or pasv first")
161 if self.pasvt then
162 self:pasvconnect()
163 end
164 local argument = sendt.argument or unescapeurl(gsub(sendt.path or "", "^[/\\]", ""))
165 if argument == "" then
166 argument = nil
167 end
168 local command = sendt.command or "stor"
169 try(tp:command(command, argument))
170 local code, reply = try(tp:check{"2..", "1.."})
171 if not self.pasvt then
172 self:portconnect()
173 end
174 local step = sendt.step or pumpstep
175 local readt = { tp }
176 local checkstep = function(src, snk)
177 local readyt = selectsocket(readt, nil, 0)
178 if readyt[tp] then
179 code = try(tp:check("2.."))
180 end
181 return step(src, snk)
182 end
183 local sink = sinksocket("close-when-done", self.data)
184 try(pumpall(sendt.source, sink, checkstep))
185 if find(code, "1..") then
186 try(tp:check("2.."))
187 end
188 self.data:close()
189 local sent = skipsocket(1, self.data:getstats())
190 self.data = nil
191 return sent
192end
193
194function methods.receive(self, recvt)
195 local try = self.try
196 local tp = self.tp
197 try(self.pasvt or self.server, "need port or pasv first")
198 if self.pasvt then self:pasvconnect() end
199 local argument = recvt.argument or unescapeurl(gsub(recvt.path or "", "^[/\\]", ""))
200 if argument == "" then
201 argument = nil
202 end
203 local command = recvt.command or "retr"
204 try(tp:command(command, argument))
205 local code,reply = try(tp:check{"1..", "2.."})
206 if code >= 200 and code <= 299 then
207 recvt.sink(reply)
208 return 1
209 end
210 if not self.pasvt then
211 self:portconnect()
212 end
213 local source = sourcesocket("until-closed", self.data)
214 local step = recvt.step or pumpstep
215 try(pumpall(source, recvt.sink, step))
216 if find(code, "1..") then
217 try(tp:check("2.."))
218 end
219 self.data:close()
220 self.data = nil
221 return 1
222end
223
224function methods.cwd(self, dir)
225 local try = self.try
226 local tp = self.tp
227 try(tp:command("cwd", dir))
228 try(tp:check(250))
229 return 1
230end
231
232function methods.type(self, typ)
233 local try = self.try
234 local tp = self.tp
235 try(tp:command("type", typ))
236 try(tp:check(200))
237 return 1
238end
239
240function methods.greet(self)
241 local try = self.try
242 local tp = self.tp
243 local code = try(tp:check{"1..", "2.."})
244 if find(code, "1..") then
245 try(tp:check("2.."))
246 end
247 return 1
248end
249
250function methods.quit(self)
251 local try = self.try
252 try(self.tp:command("quit"))
253 try(self.tp:check("2.."))
254 return 1
255end
256
257function methods.close(self)
258 local data = self.data
259 if data then
260 data:close()
261 end
262 local server = self.server
263 if server then
264 server:close()
265 end
266 local tp = self.tp
267 if tp then
268 tp:close()
269 end
270end
271
272local function override(t)
273 if t.url then
274 local u = parseurl(t.url)
275 for k, v in next, t do
276 u[k] = v
277 end
278 return u
279 else
280 return t
281 end
282end
283
284local function tput(putt)
285 putt = override(putt)
286 local host = putt.host
287 trysocket(host, "missing hostname")
288 local f = ftp.open(host, putt.port, putt.create)
289 f:greet()
290 f:login(putt.user, putt.password)
291 local typ = putt.type
292 if typ then
293 f:type(typ)
294 end
295 f:epsv()
296 local sent = f:send(putt)
297 f:quit()
298 f:close()
299 return sent
300end
301
302local default = {
303 path = "/",
304 scheme = "ftp",
305}
306
307local function genericform(u)
308 local t = trysocket(parseurl(u, default))
309 trysocket(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'")
310 trysocket(t.host, "missing hostname")
311 local pat = "^type=(.)$"
312 if t.params then
313 local typ = skipsocket(2, find(t.params, pat))
314 t.type = typ
315 trysocket(typ == "a" or typ == "i", "invalid type '" .. typ .. "'")
316 end
317 return t
318end
319
320ftp.genericform = genericform
321
322local function sput(u, body)
323 local putt = genericform(u)
324 putt.source = sourcestring(body)
325 return tput(putt)
326end
327
328ftp.put = protectsocket(function(putt, body)
329 if type(putt) == "string" then
330 return sput(putt, body)
331 else
332 return tput(putt)
333 end
334end)
335
336local function tget(gett)
337 gett = override(gett)
338 local host = gett.host
339 trysocket(host, "missing hostname")
340 local f = ftp.open(host, gett.port, gett.create)
341 f:greet()
342 f:login(gett.user, gett.password)
343 if gett.type then
344 f:type(gett.type)
345 end
346 f:epsv()
347 f:receive(gett)
348 f:quit()
349 return f:close()
350end
351
352local function sget(u)
353 local gett = genericform(u)
354 local t = { }
355 gett.sink = sinktable(t)
356 tget(gett)
357 return concat(t)
358end
359
360ftp.command = protectsocket(function(cmdt)
361 cmdt = override(cmdt)
362 local command = cmdt.command
363 local argument = cmdt.argument
364 local check = cmdt.check
365 local host = cmdt.host
366 trysocket(host, "missing hostname")
367 trysocket(command, "missing command")
368 local f = ftp.open(host, cmdt.port, cmdt.create)
369 local try = f.try
370 local tp = f.tp
371 f:greet()
372 f:login(cmdt.user, cmdt.password)
373 if type(command) == "table" then
374 local argument = argument or { }
375 for i=1,#command do
376 local cmd = command[i]
377 try(tp:command(cmd, argument[i]))
378 if check and check[i] then
379 try(tp:check(check[i]))
380 end
381 end
382 else
383 try(tp:command(command, argument))
384 if check then
385 try(tp:check(check))
386 end
387 end
388 f:quit()
389 return f:close()
390end)
391
392ftp.get = protectsocket(function(gett)
393 if type(gett) == "string" then
394 return sget(gett)
395 else
396 return tget(gett)
397 end
398end)
399
400package.loaded["socket.ftp"] = ftp
401
402return ftp
403 |