util-soc-imp-ftp.lua /size: 10 Kb    last modification: 2020-07-01 14:35
1-- original file : ftp.lua
2-- for more into : see util-soc.lua
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    -- so we try a table or string ?
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