atom0s
8 years ago
10 changed files with 1918 additions and 0 deletions
@ -0,0 +1,292 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- LTN12 - Filters, sources, sinks and pumps. |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: ltn12.lua,v 1.31 2006/04/03 04:45:42 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local string = require("string") |
||||||
|
local table = require("table") |
||||||
|
local base = _G |
||||||
|
module("ltn12") |
||||||
|
|
||||||
|
filter = {} |
||||||
|
source = {} |
||||||
|
sink = {} |
||||||
|
pump = {} |
||||||
|
|
||||||
|
-- 2048 seems to be better in windows... |
||||||
|
BLOCKSIZE = 2048 |
||||||
|
_VERSION = "LTN12 1.0.1" |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Filter stuff |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- returns a high level filter that cycles a low-level filter |
||||||
|
function filter.cycle(low, ctx, extra) |
||||||
|
base.assert(low) |
||||||
|
return function(chunk) |
||||||
|
local ret |
||||||
|
ret, ctx = low(ctx, chunk, extra) |
||||||
|
return ret |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- chains a bunch of filters together |
||||||
|
-- (thanks to Wim Couwenberg) |
||||||
|
function filter.chain(...) |
||||||
|
local n = table.getn(arg) |
||||||
|
local top, index = 1, 1 |
||||||
|
local retry = "" |
||||||
|
return function(chunk) |
||||||
|
retry = chunk and retry |
||||||
|
while true do |
||||||
|
if index == top then |
||||||
|
chunk = arg[index](chunk) |
||||||
|
if chunk == "" or top == n then return chunk |
||||||
|
elseif chunk then index = index + 1 |
||||||
|
else |
||||||
|
top = top+1 |
||||||
|
index = top |
||||||
|
end |
||||||
|
else |
||||||
|
chunk = arg[index](chunk or "") |
||||||
|
if chunk == "" then |
||||||
|
index = index - 1 |
||||||
|
chunk = retry |
||||||
|
elseif chunk then |
||||||
|
if index == n then return chunk |
||||||
|
else index = index + 1 end |
||||||
|
else base.error("filter returned inappropriate nil") end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Source stuff |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- create an empty source |
||||||
|
local function empty() |
||||||
|
return nil |
||||||
|
end |
||||||
|
|
||||||
|
function source.empty() |
||||||
|
return empty |
||||||
|
end |
||||||
|
|
||||||
|
-- returns a source that just outputs an error |
||||||
|
function source.error(err) |
||||||
|
return function() |
||||||
|
return nil, err |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- creates a file source |
||||||
|
function source.file(handle, io_err) |
||||||
|
if handle then |
||||||
|
return function() |
||||||
|
local chunk = handle:read(BLOCKSIZE) |
||||||
|
if not chunk then handle:close() end |
||||||
|
return chunk |
||||||
|
end |
||||||
|
else return source.error(io_err or "unable to open file") end |
||||||
|
end |
||||||
|
|
||||||
|
-- turns a fancy source into a simple source |
||||||
|
function source.simplify(src) |
||||||
|
base.assert(src) |
||||||
|
return function() |
||||||
|
local chunk, err_or_new = src() |
||||||
|
src = err_or_new or src |
||||||
|
if not chunk then return nil, err_or_new |
||||||
|
else return chunk end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- creates string source |
||||||
|
function source.string(s) |
||||||
|
if s then |
||||||
|
local i = 1 |
||||||
|
return function() |
||||||
|
local chunk = string.sub(s, i, i+BLOCKSIZE-1) |
||||||
|
i = i + BLOCKSIZE |
||||||
|
if chunk ~= "" then return chunk |
||||||
|
else return nil end |
||||||
|
end |
||||||
|
else return source.empty() end |
||||||
|
end |
||||||
|
|
||||||
|
-- creates rewindable source |
||||||
|
function source.rewind(src) |
||||||
|
base.assert(src) |
||||||
|
local t = {} |
||||||
|
return function(chunk) |
||||||
|
if not chunk then |
||||||
|
chunk = table.remove(t) |
||||||
|
if not chunk then return src() |
||||||
|
else return chunk end |
||||||
|
else |
||||||
|
table.insert(t, chunk) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
function source.chain(src, f) |
||||||
|
base.assert(src and f) |
||||||
|
local last_in, last_out = "", "" |
||||||
|
local state = "feeding" |
||||||
|
local err |
||||||
|
return function() |
||||||
|
if not last_out then |
||||||
|
base.error('source is empty!', 2) |
||||||
|
end |
||||||
|
while true do |
||||||
|
if state == "feeding" then |
||||||
|
last_in, err = src() |
||||||
|
if err then return nil, err end |
||||||
|
last_out = f(last_in) |
||||||
|
if not last_out then |
||||||
|
if last_in then |
||||||
|
base.error('filter returned inappropriate nil') |
||||||
|
else |
||||||
|
return nil |
||||||
|
end |
||||||
|
elseif last_out ~= "" then |
||||||
|
state = "eating" |
||||||
|
if last_in then last_in = "" end |
||||||
|
return last_out |
||||||
|
end |
||||||
|
else |
||||||
|
last_out = f(last_in) |
||||||
|
if last_out == "" then |
||||||
|
if last_in == "" then |
||||||
|
state = "feeding" |
||||||
|
else |
||||||
|
base.error('filter returned ""') |
||||||
|
end |
||||||
|
elseif not last_out then |
||||||
|
if last_in then |
||||||
|
base.error('filter returned inappropriate nil') |
||||||
|
else |
||||||
|
return nil |
||||||
|
end |
||||||
|
else |
||||||
|
return last_out |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- creates a source that produces contents of several sources, one after the |
||||||
|
-- other, as if they were concatenated |
||||||
|
-- (thanks to Wim Couwenberg) |
||||||
|
function source.cat(...) |
||||||
|
local src = table.remove(arg, 1) |
||||||
|
return function() |
||||||
|
while src do |
||||||
|
local chunk, err = src() |
||||||
|
if chunk then return chunk end |
||||||
|
if err then return nil, err end |
||||||
|
src = table.remove(arg, 1) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Sink stuff |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- creates a sink that stores into a table |
||||||
|
function sink.table(t) |
||||||
|
t = t or {} |
||||||
|
local f = function(chunk, err) |
||||||
|
if chunk then table.insert(t, chunk) end |
||||||
|
return 1 |
||||||
|
end |
||||||
|
return f, t |
||||||
|
end |
||||||
|
|
||||||
|
-- turns a fancy sink into a simple sink |
||||||
|
function sink.simplify(snk) |
||||||
|
base.assert(snk) |
||||||
|
return function(chunk, err) |
||||||
|
local ret, err_or_new = snk(chunk, err) |
||||||
|
if not ret then return nil, err_or_new end |
||||||
|
snk = err_or_new or snk |
||||||
|
return 1 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- creates a file sink |
||||||
|
function sink.file(handle, io_err) |
||||||
|
if handle then |
||||||
|
return function(chunk, err) |
||||||
|
if not chunk then |
||||||
|
handle:close() |
||||||
|
return 1 |
||||||
|
else return handle:write(chunk) end |
||||||
|
end |
||||||
|
else return sink.error(io_err or "unable to open file") end |
||||||
|
end |
||||||
|
|
||||||
|
-- creates a sink that discards data |
||||||
|
local function null() |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function sink.null() |
||||||
|
return null |
||||||
|
end |
||||||
|
|
||||||
|
-- creates a sink that just returns an error |
||||||
|
function sink.error(err) |
||||||
|
return function() |
||||||
|
return nil, err |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- chains a sink with a filter |
||||||
|
function sink.chain(f, snk) |
||||||
|
base.assert(f and snk) |
||||||
|
return function(chunk, err) |
||||||
|
if chunk ~= "" then |
||||||
|
local filtered = f(chunk) |
||||||
|
local done = chunk and "" |
||||||
|
while true do |
||||||
|
local ret, snkerr = snk(filtered, err) |
||||||
|
if not ret then return nil, snkerr end |
||||||
|
if filtered == done then return 1 end |
||||||
|
filtered = f(done) |
||||||
|
end |
||||||
|
else return 1 end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Pump stuff |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- pumps one chunk from the source to the sink |
||||||
|
function pump.step(src, snk) |
||||||
|
local chunk, src_err = src() |
||||||
|
local ret, snk_err = snk(chunk, src_err) |
||||||
|
if chunk and ret then return 1 |
||||||
|
else return nil, src_err or snk_err end |
||||||
|
end |
||||||
|
|
||||||
|
-- pumps all data from a source to a sink, using a step function |
||||||
|
function pump.all(src, snk, step) |
||||||
|
base.assert(src and snk) |
||||||
|
step = step or pump.step |
||||||
|
while true do |
||||||
|
local ret, err = step(src, snk) |
||||||
|
if not ret then |
||||||
|
if err then return nil, err |
||||||
|
else return 1 end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
@ -0,0 +1,87 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- MIME support for the Lua language. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- Conforming to RFCs 2045-2049 |
||||||
|
-- RCS ID: $Id: mime.lua,v 1.29 2007/06/11 23:44:54 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module and import dependencies |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local base = _G |
||||||
|
local ltn12 = require("ltn12") |
||||||
|
local mime = require("mime.core") |
||||||
|
local io = require("io") |
||||||
|
local string = require("string") |
||||||
|
module("mime") |
||||||
|
|
||||||
|
-- encode, decode and wrap algorithm tables |
||||||
|
encodet = {} |
||||||
|
decodet = {} |
||||||
|
wrapt = {} |
||||||
|
|
||||||
|
-- creates a function that chooses a filter by name from a given table |
||||||
|
local function choose(table) |
||||||
|
return function(name, opt1, opt2) |
||||||
|
if base.type(name) ~= "string" then |
||||||
|
name, opt1, opt2 = "default", name, opt1 |
||||||
|
end |
||||||
|
local f = table[name or "nil"] |
||||||
|
if not f then |
||||||
|
base.error("unknown key (" .. base.tostring(name) .. ")", 3) |
||||||
|
else return f(opt1, opt2) end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- define the encoding filters |
||||||
|
encodet['base64'] = function() |
||||||
|
return ltn12.filter.cycle(b64, "") |
||||||
|
end |
||||||
|
|
||||||
|
encodet['quoted-printable'] = function(mode) |
||||||
|
return ltn12.filter.cycle(qp, "", |
||||||
|
(mode == "binary") and "=0D=0A" or "\r\n") |
||||||
|
end |
||||||
|
|
||||||
|
-- define the decoding filters |
||||||
|
decodet['base64'] = function() |
||||||
|
return ltn12.filter.cycle(unb64, "") |
||||||
|
end |
||||||
|
|
||||||
|
decodet['quoted-printable'] = function() |
||||||
|
return ltn12.filter.cycle(unqp, "") |
||||||
|
end |
||||||
|
|
||||||
|
local function format(chunk) |
||||||
|
if chunk then |
||||||
|
if chunk == "" then return "''" |
||||||
|
else return string.len(chunk) end |
||||||
|
else return "nil" end |
||||||
|
end |
||||||
|
|
||||||
|
-- define the line-wrap filters |
||||||
|
wrapt['text'] = function(length) |
||||||
|
length = length or 76 |
||||||
|
return ltn12.filter.cycle(wrp, length, length) |
||||||
|
end |
||||||
|
wrapt['base64'] = wrapt['text'] |
||||||
|
wrapt['default'] = wrapt['text'] |
||||||
|
|
||||||
|
wrapt['quoted-printable'] = function() |
||||||
|
return ltn12.filter.cycle(qpwrp, 76, 76) |
||||||
|
end |
||||||
|
|
||||||
|
-- function that choose the encoding, decoding or wrap algorithm |
||||||
|
encode = choose(encodet) |
||||||
|
decode = choose(decodet) |
||||||
|
wrap = choose(wrapt) |
||||||
|
|
||||||
|
-- define the end-of-line normalization filter |
||||||
|
function normalize(marker) |
||||||
|
return ltn12.filter.cycle(eol, 0, marker) |
||||||
|
end |
||||||
|
|
||||||
|
-- high level stuffing filter |
||||||
|
function stuff() |
||||||
|
return ltn12.filter.cycle(dot, 2) |
||||||
|
end |
@ -0,0 +1,132 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- LuaSocket helper module |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module and import dependencies |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local base = _G |
||||||
|
local string = require("string") |
||||||
|
local math = require("math") |
||||||
|
local socket = require("socket.core") |
||||||
|
module("socket") |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Exported auxiliar functions |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function connect(address, port, laddress, lport) |
||||||
|
local sock, err = socket.tcp() |
||||||
|
if not sock then return nil, err end |
||||||
|
if laddress then |
||||||
|
local res, err = sock:bind(laddress, lport, -1) |
||||||
|
if not res then return nil, err end |
||||||
|
end |
||||||
|
local res, err = sock:connect(address, port) |
||||||
|
if not res then return nil, err end |
||||||
|
return sock |
||||||
|
end |
||||||
|
|
||||||
|
function bind(host, port, backlog) |
||||||
|
local sock, err = socket.tcp() |
||||||
|
if not sock then return nil, err end |
||||||
|
sock:setoption("reuseaddr", true) |
||||||
|
local res, err = sock:bind(host, port) |
||||||
|
if not res then return nil, err end |
||||||
|
res, err = sock:listen(backlog) |
||||||
|
if not res then return nil, err end |
||||||
|
return sock |
||||||
|
end |
||||||
|
|
||||||
|
try = newtry() |
||||||
|
|
||||||
|
function choose(table) |
||||||
|
return function(name, opt1, opt2) |
||||||
|
if base.type(name) ~= "string" then |
||||||
|
name, opt1, opt2 = "default", name, opt1 |
||||||
|
end |
||||||
|
local f = table[name or "nil"] |
||||||
|
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) |
||||||
|
else return f(opt1, opt2) end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Socket sources and sinks, conforming to LTN12 |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- create namespaces inside LuaSocket namespace |
||||||
|
sourcet = {} |
||||||
|
sinkt = {} |
||||||
|
|
||||||
|
BLOCKSIZE = 2048 |
||||||
|
|
||||||
|
sinkt["close-when-done"] = function(sock) |
||||||
|
return base.setmetatable({ |
||||||
|
getfd = function() return sock:getfd() end, |
||||||
|
dirty = function() return sock:dirty() end |
||||||
|
}, { |
||||||
|
__call = function(self, chunk, err) |
||||||
|
if not chunk then |
||||||
|
sock:close() |
||||||
|
return 1 |
||||||
|
else return sock:send(chunk) end |
||||||
|
end |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
sinkt["keep-open"] = function(sock) |
||||||
|
return base.setmetatable({ |
||||||
|
getfd = function() return sock:getfd() end, |
||||||
|
dirty = function() return sock:dirty() end |
||||||
|
}, { |
||||||
|
__call = function(self, chunk, err) |
||||||
|
if chunk then return sock:send(chunk) |
||||||
|
else return 1 end |
||||||
|
end |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
sinkt["default"] = sinkt["keep-open"] |
||||||
|
|
||||||
|
sink = choose(sinkt) |
||||||
|
|
||||||
|
sourcet["by-length"] = function(sock, length) |
||||||
|
return base.setmetatable({ |
||||||
|
getfd = function() return sock:getfd() end, |
||||||
|
dirty = function() return sock:dirty() end |
||||||
|
}, { |
||||||
|
__call = function() |
||||||
|
if length <= 0 then return nil end |
||||||
|
local size = math.min(socket.BLOCKSIZE, length) |
||||||
|
local chunk, err = sock:receive(size) |
||||||
|
if err then return nil, err end |
||||||
|
length = length - string.len(chunk) |
||||||
|
return chunk |
||||||
|
end |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
sourcet["until-closed"] = function(sock) |
||||||
|
local done |
||||||
|
return base.setmetatable({ |
||||||
|
getfd = function() return sock:getfd() end, |
||||||
|
dirty = function() return sock:dirty() end |
||||||
|
}, { |
||||||
|
__call = function() |
||||||
|
if done then return nil end |
||||||
|
local chunk, err, partial = sock:receive(socket.BLOCKSIZE) |
||||||
|
if not err then return chunk |
||||||
|
elseif err == "closed" then |
||||||
|
sock:close() |
||||||
|
done = 1 |
||||||
|
return partial |
||||||
|
else return nil, err end |
||||||
|
end |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
sourcet["default"] = sourcet["until-closed"] |
||||||
|
|
||||||
|
source = choose(sourcet) |
@ -0,0 +1,281 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- FTP support for the Lua language |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: ftp.lua,v 1.45 2007/07/11 19:25:47 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module and import dependencies |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local base = _G |
||||||
|
local table = require("table") |
||||||
|
local string = require("string") |
||||||
|
local math = require("math") |
||||||
|
local socket = require("socket") |
||||||
|
local url = require("socket.url") |
||||||
|
local tp = require("socket.tp") |
||||||
|
local ltn12 = require("ltn12") |
||||||
|
module("socket.ftp") |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Program constants |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- timeout in seconds before the program gives up on a connection |
||||||
|
TIMEOUT = 60 |
||||||
|
-- default port for ftp service |
||||||
|
PORT = 21 |
||||||
|
-- this is the default anonymous password. used when no password is |
||||||
|
-- provided in url. should be changed to your e-mail. |
||||||
|
USER = "ftp" |
||||||
|
PASSWORD = "[email protected]" |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Low level FTP API |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local metat = { __index = {} } |
||||||
|
|
||||||
|
function open(server, port, create) |
||||||
|
local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create)) |
||||||
|
local f = base.setmetatable({ tp = tp }, metat) |
||||||
|
-- make sure everything gets closed in an exception |
||||||
|
f.try = socket.newtry(function() f:close() end) |
||||||
|
return f |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:portconnect() |
||||||
|
self.try(self.server:settimeout(TIMEOUT)) |
||||||
|
self.data = self.try(self.server:accept()) |
||||||
|
self.try(self.data:settimeout(TIMEOUT)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:pasvconnect() |
||||||
|
self.data = self.try(socket.tcp()) |
||||||
|
self.try(self.data:settimeout(TIMEOUT)) |
||||||
|
self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:login(user, password) |
||||||
|
self.try(self.tp:command("user", user or USER)) |
||||||
|
local code, reply = self.try(self.tp:check{"2..", 331}) |
||||||
|
if code == 331 then |
||||||
|
self.try(self.tp:command("pass", password or PASSWORD)) |
||||||
|
self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:pasv() |
||||||
|
self.try(self.tp:command("pasv")) |
||||||
|
local code, reply = self.try(self.tp:check("2..")) |
||||||
|
local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" |
||||||
|
local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) |
||||||
|
self.try(a and b and c and d and p1 and p2, reply) |
||||||
|
self.pasvt = { |
||||||
|
ip = string.format("%d.%d.%d.%d", a, b, c, d), |
||||||
|
port = p1*256 + p2 |
||||||
|
} |
||||||
|
if self.server then |
||||||
|
self.server:close() |
||||||
|
self.server = nil |
||||||
|
end |
||||||
|
return self.pasvt.ip, self.pasvt.port |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:port(ip, port) |
||||||
|
self.pasvt = nil |
||||||
|
if not ip then |
||||||
|
ip, port = self.try(self.tp:getcontrol():getsockname()) |
||||||
|
self.server = self.try(socket.bind(ip, 0)) |
||||||
|
ip, port = self.try(self.server:getsockname()) |
||||||
|
self.try(self.server:settimeout(TIMEOUT)) |
||||||
|
end |
||||||
|
local pl = math.mod(port, 256) |
||||||
|
local ph = (port - pl)/256 |
||||||
|
local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") |
||||||
|
self.try(self.tp:command("port", arg)) |
||||||
|
self.try(self.tp:check("2..")) |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:send(sendt) |
||||||
|
self.try(self.pasvt or self.server, "need port or pasv first") |
||||||
|
-- if there is a pasvt table, we already sent a PASV command |
||||||
|
-- we just get the data connection into self.data |
||||||
|
if self.pasvt then self:pasvconnect() end |
||||||
|
-- get the transfer argument and command |
||||||
|
local argument = sendt.argument or |
||||||
|
url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) |
||||||
|
if argument == "" then argument = nil end |
||||||
|
local command = sendt.command or "stor" |
||||||
|
-- send the transfer command and check the reply |
||||||
|
self.try(self.tp:command(command, argument)) |
||||||
|
local code, reply = self.try(self.tp:check{"2..", "1.."}) |
||||||
|
-- if there is not a a pasvt table, then there is a server |
||||||
|
-- and we already sent a PORT command |
||||||
|
if not self.pasvt then self:portconnect() end |
||||||
|
-- get the sink, source and step for the transfer |
||||||
|
local step = sendt.step or ltn12.pump.step |
||||||
|
local readt = {self.tp.c} |
||||||
|
local checkstep = function(src, snk) |
||||||
|
-- check status in control connection while downloading |
||||||
|
local readyt = socket.select(readt, nil, 0) |
||||||
|
if readyt[tp] then code = self.try(self.tp:check("2..")) end |
||||||
|
return step(src, snk) |
||||||
|
end |
||||||
|
local sink = socket.sink("close-when-done", self.data) |
||||||
|
-- transfer all data and check error |
||||||
|
self.try(ltn12.pump.all(sendt.source, sink, checkstep)) |
||||||
|
if string.find(code, "1..") then self.try(self.tp:check("2..")) end |
||||||
|
-- done with data connection |
||||||
|
self.data:close() |
||||||
|
-- find out how many bytes were sent |
||||||
|
local sent = socket.skip(1, self.data:getstats()) |
||||||
|
self.data = nil |
||||||
|
return sent |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:receive(recvt) |
||||||
|
self.try(self.pasvt or self.server, "need port or pasv first") |
||||||
|
if self.pasvt then self:pasvconnect() end |
||||||
|
local argument = recvt.argument or |
||||||
|
url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) |
||||||
|
if argument == "" then argument = nil end |
||||||
|
local command = recvt.command or "retr" |
||||||
|
self.try(self.tp:command(command, argument)) |
||||||
|
local code = self.try(self.tp:check{"1..", "2.."}) |
||||||
|
if not self.pasvt then self:portconnect() end |
||||||
|
local source = socket.source("until-closed", self.data) |
||||||
|
local step = recvt.step or ltn12.pump.step |
||||||
|
self.try(ltn12.pump.all(source, recvt.sink, step)) |
||||||
|
if string.find(code, "1..") then self.try(self.tp:check("2..")) end |
||||||
|
self.data:close() |
||||||
|
self.data = nil |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:cwd(dir) |
||||||
|
self.try(self.tp:command("cwd", dir)) |
||||||
|
self.try(self.tp:check(250)) |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:type(type) |
||||||
|
self.try(self.tp:command("type", type)) |
||||||
|
self.try(self.tp:check(200)) |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:greet() |
||||||
|
local code = self.try(self.tp:check{"1..", "2.."}) |
||||||
|
if string.find(code, "1..") then self.try(self.tp:check("2..")) end |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:quit() |
||||||
|
self.try(self.tp:command("quit")) |
||||||
|
self.try(self.tp:check("2..")) |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:close() |
||||||
|
if self.data then self.data:close() end |
||||||
|
if self.server then self.server:close() end |
||||||
|
return self.tp:close() |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- High level FTP API |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local function override(t) |
||||||
|
if t.url then |
||||||
|
local u = url.parse(t.url) |
||||||
|
for i,v in base.pairs(t) do |
||||||
|
u[i] = v |
||||||
|
end |
||||||
|
return u |
||||||
|
else return t end |
||||||
|
end |
||||||
|
|
||||||
|
local function tput(putt) |
||||||
|
putt = override(putt) |
||||||
|
socket.try(putt.host, "missing hostname") |
||||||
|
local f = open(putt.host, putt.port, putt.create) |
||||||
|
f:greet() |
||||||
|
f:login(putt.user, putt.password) |
||||||
|
if putt.type then f:type(putt.type) end |
||||||
|
f:pasv() |
||||||
|
local sent = f:send(putt) |
||||||
|
f:quit() |
||||||
|
f:close() |
||||||
|
return sent |
||||||
|
end |
||||||
|
|
||||||
|
local default = { |
||||||
|
path = "/", |
||||||
|
scheme = "ftp" |
||||||
|
} |
||||||
|
|
||||||
|
local function parse(u) |
||||||
|
local t = socket.try(url.parse(u, default)) |
||||||
|
socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") |
||||||
|
socket.try(t.host, "missing hostname") |
||||||
|
local pat = "^type=(.)$" |
||||||
|
if t.params then |
||||||
|
t.type = socket.skip(2, string.find(t.params, pat)) |
||||||
|
socket.try(t.type == "a" or t.type == "i", |
||||||
|
"invalid type '" .. t.type .. "'") |
||||||
|
end |
||||||
|
return t |
||||||
|
end |
||||||
|
|
||||||
|
local function sput(u, body) |
||||||
|
local putt = parse(u) |
||||||
|
putt.source = ltn12.source.string(body) |
||||||
|
return tput(putt) |
||||||
|
end |
||||||
|
|
||||||
|
put = socket.protect(function(putt, body) |
||||||
|
if base.type(putt) == "string" then return sput(putt, body) |
||||||
|
else return tput(putt) end |
||||||
|
end) |
||||||
|
|
||||||
|
local function tget(gett) |
||||||
|
gett = override(gett) |
||||||
|
socket.try(gett.host, "missing hostname") |
||||||
|
local f = open(gett.host, gett.port, gett.create) |
||||||
|
f:greet() |
||||||
|
f:login(gett.user, gett.password) |
||||||
|
if gett.type then f:type(gett.type) end |
||||||
|
f:pasv() |
||||||
|
f:receive(gett) |
||||||
|
f:quit() |
||||||
|
return f:close() |
||||||
|
end |
||||||
|
|
||||||
|
local function sget(u) |
||||||
|
local gett = parse(u) |
||||||
|
local t = {} |
||||||
|
gett.sink = ltn12.sink.table(t) |
||||||
|
tget(gett) |
||||||
|
return table.concat(t) |
||||||
|
end |
||||||
|
|
||||||
|
command = socket.protect(function(cmdt) |
||||||
|
cmdt = override(cmdt) |
||||||
|
socket.try(cmdt.host, "missing hostname") |
||||||
|
socket.try(cmdt.command, "missing command") |
||||||
|
local f = open(cmdt.host, cmdt.port, cmdt.create) |
||||||
|
f:greet() |
||||||
|
f:login(cmdt.user, cmdt.password) |
||||||
|
f.try(f.tp:command(cmdt.command, cmdt.argument)) |
||||||
|
if cmdt.check then f.try(f.tp:check(cmdt.check)) end |
||||||
|
f:quit() |
||||||
|
return f:close() |
||||||
|
end) |
||||||
|
|
||||||
|
get = socket.protect(function(gett) |
||||||
|
if base.type(gett) == "string" then return sget(gett) |
||||||
|
else return tget(gett) end |
||||||
|
end) |
||||||
|
|
@ -0,0 +1,100 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Canonic header field capitalization |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id$ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
module("socket.headers") |
||||||
|
|
||||||
|
canonic = { |
||||||
|
["accept"] = "Accept", |
||||||
|
["accept-charset"] = "Accept-Charset", |
||||||
|
["accept-encoding"] = "Accept-Encoding", |
||||||
|
["accept-language"] = "Accept-Language", |
||||||
|
["accept-ranges"] = "Accept-Ranges", |
||||||
|
["action"] = "Action", |
||||||
|
["alternate-recipient"] = "Alternate-Recipient", |
||||||
|
["age"] = "Age", |
||||||
|
["allow"] = "Allow", |
||||||
|
["arrival-date"] = "Arrival-Date", |
||||||
|
["authorization"] = "Authorization", |
||||||
|
["bcc"] = "Bcc", |
||||||
|
["cache-control"] = "Cache-Control", |
||||||
|
["cc"] = "Cc", |
||||||
|
["comments"] = "Comments", |
||||||
|
["connection"] = "Connection", |
||||||
|
["content-description"] = "Content-Description", |
||||||
|
["content-disposition"] = "Content-Disposition", |
||||||
|
["content-encoding"] = "Content-Encoding", |
||||||
|
["content-id"] = "Content-ID", |
||||||
|
["content-language"] = "Content-Language", |
||||||
|
["content-length"] = "Content-Length", |
||||||
|
["content-location"] = "Content-Location", |
||||||
|
["content-md5"] = "Content-MD5", |
||||||
|
["content-range"] = "Content-Range", |
||||||
|
["content-transfer-encoding"] = "Content-Transfer-Encoding", |
||||||
|
["content-type"] = "Content-Type", |
||||||
|
["date"] = "Date", |
||||||
|
["diagnostic-code"] = "Diagnostic-Code", |
||||||
|
["dsn-gateway"] = "DSN-Gateway", |
||||||
|
["etag"] = "ETag", |
||||||
|
["expect"] = "Expect", |
||||||
|
["expires"] = "Expires", |
||||||
|
["final-log-id"] = "Final-Log-ID", |
||||||
|
["final-recipient"] = "Final-Recipient", |
||||||
|
["from"] = "From", |
||||||
|
["host"] = "Host", |
||||||
|
["if-match"] = "If-Match", |
||||||
|
["if-modified-since"] = "If-Modified-Since", |
||||||
|
["if-none-match"] = "If-None-Match", |
||||||
|
["if-range"] = "If-Range", |
||||||
|
["if-unmodified-since"] = "If-Unmodified-Since", |
||||||
|
["in-reply-to"] = "In-Reply-To", |
||||||
|
["keywords"] = "Keywords", |
||||||
|
["last-attempt-date"] = "Last-Attempt-Date", |
||||||
|
["last-modified"] = "Last-Modified", |
||||||
|
["location"] = "Location", |
||||||
|
["max-forwards"] = "Max-Forwards", |
||||||
|
["message-id"] = "Message-ID", |
||||||
|
["mime-version"] = "MIME-Version", |
||||||
|
["original-envelope-id"] = "Original-Envelope-ID", |
||||||
|
["original-recipient"] = "Original-Recipient", |
||||||
|
["pragma"] = "Pragma", |
||||||
|
["proxy-authenticate"] = "Proxy-Authenticate", |
||||||
|
["proxy-authorization"] = "Proxy-Authorization", |
||||||
|
["range"] = "Range", |
||||||
|
["received"] = "Received", |
||||||
|
["received-from-mta"] = "Received-From-MTA", |
||||||
|
["references"] = "References", |
||||||
|
["referer"] = "Referer", |
||||||
|
["remote-mta"] = "Remote-MTA", |
||||||
|
["reply-to"] = "Reply-To", |
||||||
|
["reporting-mta"] = "Reporting-MTA", |
||||||
|
["resent-bcc"] = "Resent-Bcc", |
||||||
|
["resent-cc"] = "Resent-Cc", |
||||||
|
["resent-date"] = "Resent-Date", |
||||||
|
["resent-from"] = "Resent-From", |
||||||
|
["resent-message-id"] = "Resent-Message-ID", |
||||||
|
["resent-reply-to"] = "Resent-Reply-To", |
||||||
|
["resent-sender"] = "Resent-Sender", |
||||||
|
["resent-to"] = "Resent-To", |
||||||
|
["retry-after"] = "Retry-After", |
||||||
|
["return-path"] = "Return-Path", |
||||||
|
["sender"] = "Sender", |
||||||
|
["server"] = "Server", |
||||||
|
["smtp-remote-recipient"] = "SMTP-Remote-Recipient", |
||||||
|
["status"] = "Status", |
||||||
|
["subject"] = "Subject", |
||||||
|
["te"] = "TE", |
||||||
|
["to"] = "To", |
||||||
|
["trailer"] = "Trailer", |
||||||
|
["transfer-encoding"] = "Transfer-Encoding", |
||||||
|
["upgrade"] = "Upgrade", |
||||||
|
["user-agent"] = "User-Agent", |
||||||
|
["vary"] = "Vary", |
||||||
|
["via"] = "Via", |
||||||
|
["warning"] = "Warning", |
||||||
|
["will-retry-until"] = "Will-Retry-Until", |
||||||
|
["www-authenticate"] = "WWW-Authenticate", |
||||||
|
["x-mailer"] = "X-Mailer", |
||||||
|
} |
@ -0,0 +1,352 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- HTTP/1.1 client support for the Lua language. |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: http.lua,v 1.72 2009/05/27 09:31:35 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module and import dependencies |
||||||
|
------------------------------------------------------------------------------- |
||||||
|
local socket = require("socket") |
||||||
|
local url = require("socket.url") |
||||||
|
local ltn12 = require("ltn12") |
||||||
|
local mime = require("mime") |
||||||
|
local string = require("string") |
||||||
|
local headers = require("socket.headers") |
||||||
|
local base = _G |
||||||
|
local table = require("table") |
||||||
|
module("socket.http") |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Program constants |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- connection timeout in seconds |
||||||
|
TIMEOUT = 60 |
||||||
|
-- default port for document retrieval |
||||||
|
PORT = 80 |
||||||
|
-- user agent field sent in request |
||||||
|
USERAGENT = socket._VERSION |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Reads MIME headers from a connection, unfolding where needed |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local function receiveheaders(sock, headers) |
||||||
|
local line, name, value, err |
||||||
|
headers = headers or {} |
||||||
|
-- get first line |
||||||
|
line, err = sock:receive() |
||||||
|
if err then return nil, err end |
||||||
|
-- headers go until a blank line is found |
||||||
|
while line ~= "" do |
||||||
|
-- get field-name and value |
||||||
|
name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) |
||||||
|
if not (name and value) then return nil, "malformed reponse headers" end |
||||||
|
name = string.lower(name) |
||||||
|
-- get next line (value might be folded) |
||||||
|
line, err = sock:receive() |
||||||
|
if err then return nil, err end |
||||||
|
-- unfold any folded values |
||||||
|
while string.find(line, "^%s") do |
||||||
|
value = value .. line |
||||||
|
line = sock:receive() |
||||||
|
if err then return nil, err end |
||||||
|
end |
||||||
|
-- save pair in table |
||||||
|
if headers[name] then headers[name] = headers[name] .. ", " .. value |
||||||
|
else headers[name] = value end |
||||||
|
end |
||||||
|
return headers |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Extra sources and sinks |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
socket.sourcet["http-chunked"] = function(sock, headers) |
||||||
|
return base.setmetatable({ |
||||||
|
getfd = function() return sock:getfd() end, |
||||||
|
dirty = function() return sock:dirty() end |
||||||
|
}, { |
||||||
|
__call = function() |
||||||
|
-- get chunk size, skip extention |
||||||
|
local line, err = sock:receive() |
||||||
|
if err then return nil, err end |
||||||
|
local size = base.tonumber(string.gsub(line, ";.*", ""), 16) |
||||||
|
if not size then return nil, "invalid chunk size" end |
||||||
|
-- was it the last chunk? |
||||||
|
if size > 0 then |
||||||
|
-- if not, get chunk and skip terminating CRLF |
||||||
|
local chunk, err, part = sock:receive(size) |
||||||
|
if chunk then sock:receive() end |
||||||
|
return chunk, err |
||||||
|
else |
||||||
|
-- if it was, read trailers into headers table |
||||||
|
headers, err = receiveheaders(sock, headers) |
||||||
|
if not headers then return nil, err end |
||||||
|
end |
||||||
|
end |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
socket.sinkt["http-chunked"] = function(sock) |
||||||
|
return base.setmetatable({ |
||||||
|
getfd = function() return sock:getfd() end, |
||||||
|
dirty = function() return sock:dirty() end |
||||||
|
}, { |
||||||
|
__call = function(self, chunk, err) |
||||||
|
if not chunk then return sock:send("0\r\n\r\n") end |
||||||
|
local size = string.format("%X\r\n", string.len(chunk)) |
||||||
|
return sock:send(size .. chunk .. "\r\n") |
||||||
|
end |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Low level HTTP API |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local metat = { __index = {} } |
||||||
|
|
||||||
|
function open(host, port, create) |
||||||
|
-- create socket with user connect function, or with default |
||||||
|
local c = socket.try((create or socket.tcp)()) |
||||||
|
local h = base.setmetatable({ c = c }, metat) |
||||||
|
-- create finalized try |
||||||
|
h.try = socket.newtry(function() h:close() end) |
||||||
|
-- set timeout before connecting |
||||||
|
h.try(c:settimeout(TIMEOUT)) |
||||||
|
h.try(c:connect(host, port or PORT)) |
||||||
|
-- here everything worked |
||||||
|
return h |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:sendrequestline(method, uri) |
||||||
|
local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) |
||||||
|
return self.try(self.c:send(reqline)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:sendheaders(tosend) |
||||||
|
local canonic = headers.canonic |
||||||
|
local h = "\r\n" |
||||||
|
for f, v in base.pairs(tosend) do |
||||||
|
h = (canonic[f] or f) .. ": " .. v .. "\r\n" .. h |
||||||
|
end |
||||||
|
self.try(self.c:send(h)) |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:sendbody(headers, source, step) |
||||||
|
source = source or ltn12.source.empty() |
||||||
|
step = step or ltn12.pump.step |
||||||
|
-- if we don't know the size in advance, send chunked and hope for the best |
||||||
|
local mode = "http-chunked" |
||||||
|
if headers["content-length"] then mode = "keep-open" end |
||||||
|
return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:receivestatusline() |
||||||
|
local status = self.try(self.c:receive(5)) |
||||||
|
-- identify HTTP/0.9 responses, which do not contain a status line |
||||||
|
-- this is just a heuristic, but is what the RFC recommends |
||||||
|
if status ~= "HTTP/" then return nil, status end |
||||||
|
-- otherwise proceed reading a status line |
||||||
|
status = self.try(self.c:receive("*l", status)) |
||||||
|
local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) |
||||||
|
return self.try(base.tonumber(code), status) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:receiveheaders() |
||||||
|
return self.try(receiveheaders(self.c)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:receivebody(headers, sink, step) |
||||||
|
sink = sink or ltn12.sink.null() |
||||||
|
step = step or ltn12.pump.step |
||||||
|
local length = base.tonumber(headers["content-length"]) |
||||||
|
local t = headers["transfer-encoding"] -- shortcut |
||||||
|
local mode = "default" -- connection close |
||||||
|
if t and t ~= "identity" then mode = "http-chunked" |
||||||
|
elseif base.tonumber(headers["content-length"]) then mode = "by-length" end |
||||||
|
return self.try(ltn12.pump.all(socket.source(mode, self.c, length), |
||||||
|
sink, step)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:receive09body(status, sink, step) |
||||||
|
local source = ltn12.source.rewind(socket.source("until-closed", self.c)) |
||||||
|
source(status) |
||||||
|
return self.try(ltn12.pump.all(source, sink, step)) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:close() |
||||||
|
return self.c:close() |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- High level HTTP API |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local function adjusturi(reqt) |
||||||
|
local u = reqt |
||||||
|
-- if there is a proxy, we need the full url. otherwise, just a part. |
||||||
|
if not reqt.proxy and not PROXY then |
||||||
|
u = { |
||||||
|
path = socket.try(reqt.path, "invalid path 'nil'"), |
||||||
|
params = reqt.params, |
||||||
|
query = reqt.query, |
||||||
|
fragment = reqt.fragment |
||||||
|
} |
||||||
|
end |
||||||
|
return url.build(u) |
||||||
|
end |
||||||
|
|
||||||
|
local function adjustproxy(reqt) |
||||||
|
local proxy = reqt.proxy or PROXY |
||||||
|
if proxy then |
||||||
|
proxy = url.parse(proxy) |
||||||
|
return proxy.host, proxy.port or 3128 |
||||||
|
else |
||||||
|
return reqt.host, reqt.port |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
local function adjustheaders(reqt) |
||||||
|
-- default headers |
||||||
|
local lower = { |
||||||
|
["user-agent"] = USERAGENT, |
||||||
|
["host"] = reqt.host, |
||||||
|
["connection"] = "close, TE", |
||||||
|
["te"] = "trailers" |
||||||
|
} |
||||||
|
-- if we have authentication information, pass it along |
||||||
|
if reqt.user and reqt.password then |
||||||
|
lower["authorization"] = |
||||||
|
"Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) |
||||||
|
end |
||||||
|
-- override with user headers |
||||||
|
for i,v in base.pairs(reqt.headers or lower) do |
||||||
|
lower[string.lower(i)] = v |
||||||
|
end |
||||||
|
return lower |
||||||
|
end |
||||||
|
|
||||||
|
-- default url parts |
||||||
|
local default = { |
||||||
|
host = "", |
||||||
|
port = PORT, |
||||||
|
path ="/", |
||||||
|
scheme = "http" |
||||||
|
} |
||||||
|
|
||||||
|
local function adjustrequest(reqt) |
||||||
|
-- parse url if provided |
||||||
|
local nreqt = reqt.url and url.parse(reqt.url, default) or {} |
||||||
|
-- explicit components override url |
||||||
|
for i,v in base.pairs(reqt) do nreqt[i] = v end |
||||||
|
if nreqt.port == "" then nreqt.port = 80 end |
||||||
|
socket.try(nreqt.host and nreqt.host ~= "", |
||||||
|
"invalid host '" .. base.tostring(nreqt.host) .. "'") |
||||||
|
-- compute uri if user hasn't overriden |
||||||
|
nreqt.uri = reqt.uri or adjusturi(nreqt) |
||||||
|
-- ajust host and port if there is a proxy |
||||||
|
nreqt.host, nreqt.port = adjustproxy(nreqt) |
||||||
|
-- adjust headers in request |
||||||
|
nreqt.headers = adjustheaders(nreqt) |
||||||
|
return nreqt |
||||||
|
end |
||||||
|
|
||||||
|
local function shouldredirect(reqt, code, headers) |
||||||
|
return headers.location and |
||||||
|
string.gsub(headers.location, "%s", "") ~= "" and |
||||||
|
(reqt.redirect ~= false) and |
||||||
|
(code == 301 or code == 302 or code == 303 or code == 307) and |
||||||
|
(not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") |
||||||
|
and (not reqt.nredirects or reqt.nredirects < 5) |
||||||
|
end |
||||||
|
|
||||||
|
local function shouldreceivebody(reqt, code) |
||||||
|
if reqt.method == "HEAD" then return nil end |
||||||
|
if code == 204 or code == 304 then return nil end |
||||||
|
if code >= 100 and code < 200 then return nil end |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
-- forward declarations |
||||||
|
local trequest, tredirect |
||||||
|
|
||||||
|
function tredirect(reqt, location) |
||||||
|
local result, code, headers, status = trequest { |
||||||
|
-- the RFC says the redirect URL has to be absolute, but some |
||||||
|
-- servers do not respect that |
||||||
|
url = url.absolute(reqt.url, location), |
||||||
|
source = reqt.source, |
||||||
|
sink = reqt.sink, |
||||||
|
headers = reqt.headers, |
||||||
|
proxy = reqt.proxy, |
||||||
|
nredirects = (reqt.nredirects or 0) + 1, |
||||||
|
create = reqt.create |
||||||
|
} |
||||||
|
-- pass location header back as a hint we redirected |
||||||
|
headers = headers or {} |
||||||
|
headers.location = headers.location or location |
||||||
|
return result, code, headers, status |
||||||
|
end |
||||||
|
|
||||||
|
function trequest(reqt) |
||||||
|
-- we loop until we get what we want, or |
||||||
|
-- until we are sure there is no way to get it |
||||||
|
local nreqt = adjustrequest(reqt) |
||||||
|
local h = open(nreqt.host, nreqt.port, nreqt.create) |
||||||
|
-- send request line and headers |
||||||
|
h:sendrequestline(nreqt.method, nreqt.uri) |
||||||
|
h:sendheaders(nreqt.headers) |
||||||
|
-- if there is a body, send it |
||||||
|
if nreqt.source then |
||||||
|
h:sendbody(nreqt.headers, nreqt.source, nreqt.step) |
||||||
|
end |
||||||
|
local code, status = h:receivestatusline() |
||||||
|
-- if it is an HTTP/0.9 server, simply get the body and we are done |
||||||
|
if not code then |
||||||
|
h:receive09body(status, nreqt.sink, nreqt.step) |
||||||
|
return 1, 200 |
||||||
|
end |
||||||
|
local headers |
||||||
|
-- ignore any 100-continue messages |
||||||
|
while code == 100 do |
||||||
|
headers = h:receiveheaders() |
||||||
|
code, status = h:receivestatusline() |
||||||
|
end |
||||||
|
headers = h:receiveheaders() |
||||||
|
-- at this point we should have a honest reply from the server |
||||||
|
-- we can't redirect if we already used the source, so we report the error |
||||||
|
if shouldredirect(nreqt, code, headers) and not nreqt.source then |
||||||
|
h:close() |
||||||
|
return tredirect(reqt, headers.location) |
||||||
|
end |
||||||
|
-- here we are finally done |
||||||
|
if shouldreceivebody(nreqt, code) then |
||||||
|
h:receivebody(headers, nreqt.sink, nreqt.step) |
||||||
|
end |
||||||
|
h:close() |
||||||
|
return 1, code, headers, status |
||||||
|
end |
||||||
|
|
||||||
|
local function srequest(u, b) |
||||||
|
local t = {} |
||||||
|
local reqt = { |
||||||
|
url = u, |
||||||
|
sink = ltn12.sink.table(t) |
||||||
|
} |
||||||
|
if b then |
||||||
|
reqt.source = ltn12.source.string(b) |
||||||
|
reqt.headers = { |
||||||
|
["content-length"] = string.len(b), |
||||||
|
["content-type"] = "application/x-www-form-urlencoded" |
||||||
|
} |
||||||
|
reqt.method = "POST" |
||||||
|
end |
||||||
|
local code, headers, status = socket.skip(1, trequest(reqt)) |
||||||
|
return table.concat(t), code, headers, status |
||||||
|
end |
||||||
|
|
||||||
|
request = socket.protect(function(reqt, body) |
||||||
|
if base.type(reqt) == "string" then return srequest(reqt, body) |
||||||
|
else return trequest(reqt) end |
||||||
|
end) |
@ -0,0 +1,253 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- SMTP client support for the Lua language. |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: smtp.lua,v 1.47 2009/05/27 09:31:35 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module and import dependencies |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local base = _G |
||||||
|
local coroutine = require("coroutine") |
||||||
|
local string = require("string") |
||||||
|
local math = require("math") |
||||||
|
local os = require("os") |
||||||
|
local socket = require("socket") |
||||||
|
local tp = require("socket.tp") |
||||||
|
local ltn12 = require("ltn12") |
||||||
|
local headers = require("socket.headers") |
||||||
|
local mime = require("mime") |
||||||
|
module("socket.smtp") |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Program constants |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- timeout for connection |
||||||
|
TIMEOUT = 60 |
||||||
|
-- default server used to send e-mails |
||||||
|
SERVER = "localhost" |
||||||
|
-- default port |
||||||
|
PORT = 25 |
||||||
|
-- domain used in HELO command and default sendmail |
||||||
|
-- If we are under a CGI, try to get from environment |
||||||
|
DOMAIN = os.getenv("SERVER_NAME") or "localhost" |
||||||
|
-- default time zone (means we don't know) |
||||||
|
ZONE = "-0000" |
||||||
|
|
||||||
|
--------------------------------------------------------------------------- |
||||||
|
-- Low level SMTP API |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local metat = { __index = {} } |
||||||
|
|
||||||
|
function metat.__index:greet(domain) |
||||||
|
self.try(self.tp:check("2..")) |
||||||
|
self.try(self.tp:command("EHLO", domain or DOMAIN)) |
||||||
|
return socket.skip(1, self.try(self.tp:check("2.."))) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:mail(from) |
||||||
|
self.try(self.tp:command("MAIL", "FROM:" .. from)) |
||||||
|
return self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:rcpt(to) |
||||||
|
self.try(self.tp:command("RCPT", "TO:" .. to)) |
||||||
|
return self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:data(src, step) |
||||||
|
self.try(self.tp:command("DATA")) |
||||||
|
self.try(self.tp:check("3..")) |
||||||
|
self.try(self.tp:source(src, step)) |
||||||
|
self.try(self.tp:send("\r\n.\r\n")) |
||||||
|
return self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:quit() |
||||||
|
self.try(self.tp:command("QUIT")) |
||||||
|
return self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:close() |
||||||
|
return self.tp:close() |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:login(user, password) |
||||||
|
self.try(self.tp:command("AUTH", "LOGIN")) |
||||||
|
self.try(self.tp:check("3..")) |
||||||
|
self.try(self.tp:command(mime.b64(user))) |
||||||
|
self.try(self.tp:check("3..")) |
||||||
|
self.try(self.tp:command(mime.b64(password))) |
||||||
|
return self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:plain(user, password) |
||||||
|
local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) |
||||||
|
self.try(self.tp:command("AUTH", auth)) |
||||||
|
return self.try(self.tp:check("2..")) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:auth(user, password, ext) |
||||||
|
if not user or not password then return 1 end |
||||||
|
if string.find(ext, "AUTH[^\n]+LOGIN") then |
||||||
|
return self:login(user, password) |
||||||
|
elseif string.find(ext, "AUTH[^\n]+PLAIN") then |
||||||
|
return self:plain(user, password) |
||||||
|
else |
||||||
|
self.try(nil, "authentication not supported") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- send message or throw an exception |
||||||
|
function metat.__index:send(mailt) |
||||||
|
self:mail(mailt.from) |
||||||
|
if base.type(mailt.rcpt) == "table" then |
||||||
|
for i,v in base.ipairs(mailt.rcpt) do |
||||||
|
self:rcpt(v) |
||||||
|
end |
||||||
|
else |
||||||
|
self:rcpt(mailt.rcpt) |
||||||
|
end |
||||||
|
self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step) |
||||||
|
end |
||||||
|
|
||||||
|
function open(server, port, create) |
||||||
|
local tp = socket.try(tp.connect(server or SERVER, port or PORT, |
||||||
|
TIMEOUT, create)) |
||||||
|
local s = base.setmetatable({tp = tp}, metat) |
||||||
|
-- make sure tp is closed if we get an exception |
||||||
|
s.try = socket.newtry(function() |
||||||
|
s:close() |
||||||
|
end) |
||||||
|
return s |
||||||
|
end |
||||||
|
|
||||||
|
-- convert headers to lowercase |
||||||
|
local function lower_headers(headers) |
||||||
|
local lower = {} |
||||||
|
for i,v in base.pairs(headers or lower) do |
||||||
|
lower[string.lower(i)] = v |
||||||
|
end |
||||||
|
return lower |
||||||
|
end |
||||||
|
|
||||||
|
--------------------------------------------------------------------------- |
||||||
|
-- Multipart message source |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- returns a hopefully unique mime boundary |
||||||
|
local seqno = 0 |
||||||
|
local function newboundary() |
||||||
|
seqno = seqno + 1 |
||||||
|
return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), |
||||||
|
math.random(0, 99999), seqno) |
||||||
|
end |
||||||
|
|
||||||
|
-- send_message forward declaration |
||||||
|
local send_message |
||||||
|
|
||||||
|
-- yield the headers all at once, it's faster |
||||||
|
local function send_headers(tosend) |
||||||
|
local canonic = headers.canonic |
||||||
|
local h = "\r\n" |
||||||
|
for f,v in base.pairs(tosend) do |
||||||
|
h = (canonic[f] or f) .. ': ' .. v .. "\r\n" .. h |
||||||
|
end |
||||||
|
coroutine.yield(h) |
||||||
|
end |
||||||
|
|
||||||
|
-- yield multipart message body from a multipart message table |
||||||
|
local function send_multipart(mesgt) |
||||||
|
-- make sure we have our boundary and send headers |
||||||
|
local bd = newboundary() |
||||||
|
local headers = lower_headers(mesgt.headers or {}) |
||||||
|
headers['content-type'] = headers['content-type'] or 'multipart/mixed' |
||||||
|
headers['content-type'] = headers['content-type'] .. |
||||||
|
'; boundary="' .. bd .. '"' |
||||||
|
send_headers(headers) |
||||||
|
-- send preamble |
||||||
|
if mesgt.body.preamble then |
||||||
|
coroutine.yield(mesgt.body.preamble) |
||||||
|
coroutine.yield("\r\n") |
||||||
|
end |
||||||
|
-- send each part separated by a boundary |
||||||
|
for i, m in base.ipairs(mesgt.body) do |
||||||
|
coroutine.yield("\r\n--" .. bd .. "\r\n") |
||||||
|
send_message(m) |
||||||
|
end |
||||||
|
-- send last boundary |
||||||
|
coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") |
||||||
|
-- send epilogue |
||||||
|
if mesgt.body.epilogue then |
||||||
|
coroutine.yield(mesgt.body.epilogue) |
||||||
|
coroutine.yield("\r\n") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- yield message body from a source |
||||||
|
local function send_source(mesgt) |
||||||
|
-- make sure we have a content-type |
||||||
|
local headers = lower_headers(mesgt.headers or {}) |
||||||
|
headers['content-type'] = headers['content-type'] or |
||||||
|
'text/plain; charset="iso-8859-1"' |
||||||
|
send_headers(headers) |
||||||
|
-- send body from source |
||||||
|
while true do |
||||||
|
local chunk, err = mesgt.body() |
||||||
|
if err then coroutine.yield(nil, err) |
||||||
|
elseif chunk then coroutine.yield(chunk) |
||||||
|
else break end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
-- yield message body from a string |
||||||
|
local function send_string(mesgt) |
||||||
|
-- make sure we have a content-type |
||||||
|
local headers = lower_headers(mesgt.headers or {}) |
||||||
|
headers['content-type'] = headers['content-type'] or |
||||||
|
'text/plain; charset="iso-8859-1"' |
||||||
|
send_headers(headers) |
||||||
|
-- send body from string |
||||||
|
coroutine.yield(mesgt.body) |
||||||
|
end |
||||||
|
|
||||||
|
-- message source |
||||||
|
function send_message(mesgt) |
||||||
|
if base.type(mesgt.body) == "table" then send_multipart(mesgt) |
||||||
|
elseif base.type(mesgt.body) == "function" then send_source(mesgt) |
||||||
|
else send_string(mesgt) end |
||||||
|
end |
||||||
|
|
||||||
|
-- set defaul headers |
||||||
|
local function adjust_headers(mesgt) |
||||||
|
local lower = lower_headers(mesgt.headers) |
||||||
|
lower["date"] = lower["date"] or |
||||||
|
os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE) |
||||||
|
lower["x-mailer"] = lower["x-mailer"] or socket._VERSION |
||||||
|
-- this can't be overriden |
||||||
|
lower["mime-version"] = "1.0" |
||||||
|
return lower |
||||||
|
end |
||||||
|
|
||||||
|
function message(mesgt) |
||||||
|
mesgt.headers = adjust_headers(mesgt) |
||||||
|
-- create and return message source |
||||||
|
local co = coroutine.create(function() send_message(mesgt) end) |
||||||
|
return function() |
||||||
|
local ret, a, b = coroutine.resume(co) |
||||||
|
if ret then return a, b |
||||||
|
else return nil, a end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
--------------------------------------------------------------------------- |
||||||
|
-- High level SMTP API |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
send = socket.protect(function(mailt) |
||||||
|
local s = open(mailt.server, mailt.port, mailt.create) |
||||||
|
local ext = s:greet(mailt.domain) |
||||||
|
s:auth(mailt.user, mailt.password, ext) |
||||||
|
s:send(mailt) |
||||||
|
s:quit() |
||||||
|
return s:close() |
||||||
|
end) |
@ -0,0 +1,124 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Unified SMTP/FTP subsystem |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: tp.lua,v 1.23 2009/05/27 09:31:35 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module and import dependencies |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local base = _G |
||||||
|
local string = require("string") |
||||||
|
local socket = require("socket") |
||||||
|
local ltn12 = require("ltn12") |
||||||
|
module("socket.tp") |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Program constants |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
TIMEOUT = 60 |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Implementation |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- gets server reply (works for SMTP and FTP) |
||||||
|
local function get_reply(c) |
||||||
|
local code, current, sep |
||||||
|
local line, err = c:receive() |
||||||
|
local reply = line |
||||||
|
if err then return nil, err end |
||||||
|
code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) |
||||||
|
if not code then return nil, "invalid server reply" end |
||||||
|
if sep == "-" then -- reply is multiline |
||||||
|
repeat |
||||||
|
line, err = c:receive() |
||||||
|
if err then return nil, err end |
||||||
|
current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) |
||||||
|
reply = reply .. "\n" .. line |
||||||
|
-- reply ends with same code |
||||||
|
until code == current and sep == " " |
||||||
|
end |
||||||
|
return code, reply |
||||||
|
end |
||||||
|
|
||||||
|
-- metatable for sock object |
||||||
|
local metat = { __index = {} } |
||||||
|
|
||||||
|
function metat.__index:check(ok) |
||||||
|
local code, reply = get_reply(self.c) |
||||||
|
if not code then return nil, reply end |
||||||
|
if base.type(ok) ~= "function" then |
||||||
|
if base.type(ok) == "table" then |
||||||
|
for i, v in base.ipairs(ok) do |
||||||
|
if string.find(code, v) then |
||||||
|
return base.tonumber(code), reply |
||||||
|
end |
||||||
|
end |
||||||
|
return nil, reply |
||||||
|
else |
||||||
|
if string.find(code, ok) then return base.tonumber(code), reply |
||||||
|
else return nil, reply end |
||||||
|
end |
||||||
|
else return ok(base.tonumber(code), reply) end |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:command(cmd, arg) |
||||||
|
cmd = string.upper(cmd) |
||||||
|
if arg then |
||||||
|
return self.c:send(cmd .. " " .. arg.. "\r\n") |
||||||
|
else |
||||||
|
return self.c:send(cmd .. "\r\n") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:sink(snk, pat) |
||||||
|
local chunk, err = c:receive(pat) |
||||||
|
return snk(chunk, err) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:send(data) |
||||||
|
return self.c:send(data) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:receive(pat) |
||||||
|
return self.c:receive(pat) |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:getfd() |
||||||
|
return self.c:getfd() |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:dirty() |
||||||
|
return self.c:dirty() |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:getcontrol() |
||||||
|
return self.c |
||||||
|
end |
||||||
|
|
||||||
|
function metat.__index:source(source, step) |
||||||
|
local sink = socket.sink("keep-open", self.c) |
||||||
|
local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) |
||||||
|
return ret, err |
||||||
|
end |
||||||
|
|
||||||
|
-- closes the underlying c |
||||||
|
function metat.__index:close() |
||||||
|
self.c:close() |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
-- connect with server and return c object |
||||||
|
function connect(host, port, timeout, create) |
||||||
|
local c, e = (create or socket.tcp)() |
||||||
|
if not c then return nil, e end |
||||||
|
c:settimeout(timeout or TIMEOUT) |
||||||
|
local r, e = c:connect(host, port) |
||||||
|
if not r then |
||||||
|
c:close() |
||||||
|
return nil, e |
||||||
|
end |
||||||
|
return base.setmetatable({c = c}, metat) |
||||||
|
end |
||||||
|
|
@ -0,0 +1,297 @@ |
|||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- URI parsing, composition and relative URL resolution |
||||||
|
-- LuaSocket toolkit. |
||||||
|
-- Author: Diego Nehab |
||||||
|
-- RCS ID: $Id: url.lua,v 1.38 2006/04/03 04:45:42 diego Exp $ |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Declare module |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local string = require("string") |
||||||
|
local base = _G |
||||||
|
local table = require("table") |
||||||
|
module("socket.url") |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Module version |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
_VERSION = "URL 1.0.1" |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Encodes a string into its escaped hexadecimal representation |
||||||
|
-- Input |
||||||
|
-- s: binary string to be encoded |
||||||
|
-- Returns |
||||||
|
-- escaped representation of string binary |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function escape(s) |
||||||
|
return string.gsub(s, "([^A-Za-z0-9_])", function(c) |
||||||
|
return string.format("%%%02x", string.byte(c)) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Protects a path segment, to prevent it from interfering with the |
||||||
|
-- url parsing. |
||||||
|
-- Input |
||||||
|
-- s: binary string to be encoded |
||||||
|
-- Returns |
||||||
|
-- escaped representation of string binary |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local function make_set(t) |
||||||
|
local s = {} |
||||||
|
for i,v in base.ipairs(t) do |
||||||
|
s[t[i]] = 1 |
||||||
|
end |
||||||
|
return s |
||||||
|
end |
||||||
|
|
||||||
|
-- these are allowed withing a path segment, along with alphanum |
||||||
|
-- other characters must be escaped |
||||||
|
local segment_set = make_set { |
||||||
|
"-", "_", ".", "!", "~", "*", "'", "(", |
||||||
|
")", ":", "@", "&", "=", "+", "$", ",", |
||||||
|
} |
||||||
|
|
||||||
|
local function protect_segment(s) |
||||||
|
return string.gsub(s, "([^A-Za-z0-9_])", function (c) |
||||||
|
if segment_set[c] then return c |
||||||
|
else return string.format("%%%02x", string.byte(c)) end |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Encodes a string into its escaped hexadecimal representation |
||||||
|
-- Input |
||||||
|
-- s: binary string to be encoded |
||||||
|
-- Returns |
||||||
|
-- escaped representation of string binary |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function unescape(s) |
||||||
|
return string.gsub(s, "%%(%x%x)", function(hex) |
||||||
|
return string.char(base.tonumber(hex, 16)) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Builds a path from a base path and a relative path |
||||||
|
-- Input |
||||||
|
-- base_path |
||||||
|
-- relative_path |
||||||
|
-- Returns |
||||||
|
-- corresponding absolute path |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
local function absolute_path(base_path, relative_path) |
||||||
|
if string.sub(relative_path, 1, 1) == "/" then return relative_path end |
||||||
|
local path = string.gsub(base_path, "[^/]*$", "") |
||||||
|
path = path .. relative_path |
||||||
|
path = string.gsub(path, "([^/]*%./)", function (s) |
||||||
|
if s ~= "./" then return s else return "" end |
||||||
|
end) |
||||||
|
path = string.gsub(path, "/%.$", "/") |
||||||
|
local reduced |
||||||
|
while reduced ~= path do |
||||||
|
reduced = path |
||||||
|
path = string.gsub(reduced, "([^/]*/%.%./)", function (s) |
||||||
|
if s ~= "../../" then return "" else return s end |
||||||
|
end) |
||||||
|
end |
||||||
|
path = string.gsub(reduced, "([^/]*/%.%.)$", function (s) |
||||||
|
if s ~= "../.." then return "" else return s end |
||||||
|
end) |
||||||
|
return path |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Parses a url and returns a table with all its parts according to RFC 2396 |
||||||
|
-- The following grammar describes the names given to the URL parts |
||||||
|
-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment> |
||||||
|
-- <authority> ::= <userinfo>@<host>:<port> |
||||||
|
-- <userinfo> ::= <user>[:<password>] |
||||||
|
-- <path> :: = {<segment>/}<segment> |
||||||
|
-- Input |
||||||
|
-- url: uniform resource locator of request |
||||||
|
-- default: table with default values for each field |
||||||
|
-- Returns |
||||||
|
-- table with the following fields, where RFC naming conventions have |
||||||
|
-- been preserved: |
||||||
|
-- scheme, authority, userinfo, user, password, host, port, |
||||||
|
-- path, params, query, fragment |
||||||
|
-- Obs: |
||||||
|
-- the leading '/' in {/<path>} is considered part of <path> |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function parse(url, default) |
||||||
|
-- initialize default parameters |
||||||
|
local parsed = {} |
||||||
|
for i,v in base.pairs(default or parsed) do parsed[i] = v end |
||||||
|
-- empty url is parsed to nil |
||||||
|
if not url or url == "" then return nil, "invalid url" end |
||||||
|
-- remove whitespace |
||||||
|
-- url = string.gsub(url, "%s", "") |
||||||
|
-- get fragment |
||||||
|
url = string.gsub(url, "#(.*)$", function(f) |
||||||
|
parsed.fragment = f |
||||||
|
return "" |
||||||
|
end) |
||||||
|
-- get scheme |
||||||
|
url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", |
||||||
|
function(s) parsed.scheme = s; return "" end) |
||||||
|
-- get authority |
||||||
|
url = string.gsub(url, "^//([^/]*)", function(n) |
||||||
|
parsed.authority = n |
||||||
|
return "" |
||||||
|
end) |
||||||
|
-- get query stringing |
||||||
|
url = string.gsub(url, "%?(.*)", function(q) |
||||||
|
parsed.query = q |
||||||
|
return "" |
||||||
|
end) |
||||||
|
-- get params |
||||||
|
url = string.gsub(url, "%;(.*)", function(p) |
||||||
|
parsed.params = p |
||||||
|
return "" |
||||||
|
end) |
||||||
|
-- path is whatever was left |
||||||
|
if url ~= "" then parsed.path = url end |
||||||
|
local authority = parsed.authority |
||||||
|
if not authority then return parsed end |
||||||
|
authority = string.gsub(authority,"^([^@]*)@", |
||||||
|
function(u) parsed.userinfo = u; return "" end) |
||||||
|
authority = string.gsub(authority, ":([^:]*)$", |
||||||
|
function(p) parsed.port = p; return "" end) |
||||||
|
if authority ~= "" then parsed.host = authority end |
||||||
|
local userinfo = parsed.userinfo |
||||||
|
if not userinfo then return parsed end |
||||||
|
userinfo = string.gsub(userinfo, ":([^:]*)$", |
||||||
|
function(p) parsed.password = p; return "" end) |
||||||
|
parsed.user = userinfo |
||||||
|
return parsed |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Rebuilds a parsed URL from its components. |
||||||
|
-- Components are protected if any reserved or unallowed characters are found |
||||||
|
-- Input |
||||||
|
-- parsed: parsed URL, as returned by parse |
||||||
|
-- Returns |
||||||
|
-- a stringing with the corresponding URL |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function build(parsed) |
||||||
|
local ppath = parse_path(parsed.path or "") |
||||||
|
local url = build_path(ppath) |
||||||
|
if parsed.params then url = url .. ";" .. parsed.params end |
||||||
|
if parsed.query then url = url .. "?" .. parsed.query end |
||||||
|
local authority = parsed.authority |
||||||
|
if parsed.host then |
||||||
|
authority = parsed.host |
||||||
|
if parsed.port then authority = authority .. ":" .. parsed.port end |
||||||
|
local userinfo = parsed.userinfo |
||||||
|
if parsed.user then |
||||||
|
userinfo = parsed.user |
||||||
|
if parsed.password then |
||||||
|
userinfo = userinfo .. ":" .. parsed.password |
||||||
|
end |
||||||
|
end |
||||||
|
if userinfo then authority = userinfo .. "@" .. authority end |
||||||
|
end |
||||||
|
if authority then url = "//" .. authority .. url end |
||||||
|
if parsed.scheme then url = parsed.scheme .. ":" .. url end |
||||||
|
if parsed.fragment then url = url .. "#" .. parsed.fragment end |
||||||
|
-- url = string.gsub(url, "%s", "") |
||||||
|
return url |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Builds a absolute URL from a base and a relative URL according to RFC 2396 |
||||||
|
-- Input |
||||||
|
-- base_url |
||||||
|
-- relative_url |
||||||
|
-- Returns |
||||||
|
-- corresponding absolute url |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function absolute(base_url, relative_url) |
||||||
|
if base.type(base_url) == "table" then |
||||||
|
base_parsed = base_url |
||||||
|
base_url = build(base_parsed) |
||||||
|
else |
||||||
|
base_parsed = parse(base_url) |
||||||
|
end |
||||||
|
local relative_parsed = parse(relative_url) |
||||||
|
if not base_parsed then return relative_url |
||||||
|
elseif not relative_parsed then return base_url |
||||||
|
elseif relative_parsed.scheme then return relative_url |
||||||
|
else |
||||||
|
relative_parsed.scheme = base_parsed.scheme |
||||||
|
if not relative_parsed.authority then |
||||||
|
relative_parsed.authority = base_parsed.authority |
||||||
|
if not relative_parsed.path then |
||||||
|
relative_parsed.path = base_parsed.path |
||||||
|
if not relative_parsed.params then |
||||||
|
relative_parsed.params = base_parsed.params |
||||||
|
if not relative_parsed.query then |
||||||
|
relative_parsed.query = base_parsed.query |
||||||
|
end |
||||||
|
end |
||||||
|
else |
||||||
|
relative_parsed.path = absolute_path(base_parsed.path or "", |
||||||
|
relative_parsed.path) |
||||||
|
end |
||||||
|
end |
||||||
|
return build(relative_parsed) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Breaks a path into its segments, unescaping the segments |
||||||
|
-- Input |
||||||
|
-- path |
||||||
|
-- Returns |
||||||
|
-- segment: a table with one entry per segment |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function parse_path(path) |
||||||
|
local parsed = {} |
||||||
|
path = path or "" |
||||||
|
--path = string.gsub(path, "%s", "") |
||||||
|
string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) |
||||||
|
for i = 1, table.getn(parsed) do |
||||||
|
parsed[i] = unescape(parsed[i]) |
||||||
|
end |
||||||
|
if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end |
||||||
|
if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end |
||||||
|
return parsed |
||||||
|
end |
||||||
|
|
||||||
|
----------------------------------------------------------------------------- |
||||||
|
-- Builds a path component from its segments, escaping protected characters. |
||||||
|
-- Input |
||||||
|
-- parsed: path segments |
||||||
|
-- unsafe: if true, segments are not protected before path is built |
||||||
|
-- Returns |
||||||
|
-- path: corresponding path stringing |
||||||
|
----------------------------------------------------------------------------- |
||||||
|
function build_path(parsed, unsafe) |
||||||
|
local path = "" |
||||||
|
local n = table.getn(parsed) |
||||||
|
if unsafe then |
||||||
|
for i = 1, n-1 do |
||||||
|
path = path .. parsed[i] |
||||||
|
path = path .. "/" |
||||||
|
end |
||||||
|
if n > 0 then |
||||||
|
path = path .. parsed[n] |
||||||
|
if parsed.is_directory then path = path .. "/" end |
||||||
|
end |
||||||
|
else |
||||||
|
for i = 1, n-1 do |
||||||
|
path = path .. protect_segment(parsed[i]) |
||||||
|
path = path .. "/" |
||||||
|
end |
||||||
|
if n > 0 then |
||||||
|
path = path .. protect_segment(parsed[n]) |
||||||
|
if parsed.is_directory then path = path .. "/" end |
||||||
|
end |
||||||
|
end |
||||||
|
if parsed.is_absolute then path = "/" .. path end |
||||||
|
return path |
||||||
|
end |
Binary file not shown.
Loading…
Reference in new issue