FTP low-level working.

SMTP connection oriented working.
ltn12 improved.
This commit is contained in:
Diego Nehab 2004-05-25 05:27:44 +00:00
parent 4fc164b8ea
commit 888496aa82
15 changed files with 701 additions and 659 deletions

View file

@ -13,27 +13,6 @@
/*=========================================================================*\
* Exported functions
\*=========================================================================*/
/*-------------------------------------------------------------------------*\
* Prints the value of a class in a nice way
\*-------------------------------------------------------------------------*/
int aux_meth_tostring(lua_State *L)
{
char buf[32];
if (!lua_getmetatable(L, 1)) goto error;
lua_pushstring(L, "__index");
lua_gettable(L, -2);
if (!lua_istable(L, -1)) goto error;
lua_pushstring(L, "class");
lua_gettable(L, -2);
if (!lua_isstring(L, -1)) goto error;
sprintf(buf, "%p", lua_touserdata(L, 1));
lua_pushfstring(L, "socket: %s: %s", lua_tostring(L, -1), buf);
return 1;
error:
lua_pushnil(L);
return 1;
}
/*-------------------------------------------------------------------------*\
* Initializes the module
\*-------------------------------------------------------------------------*/
@ -48,23 +27,20 @@ int aux_open(lua_State *L)
void aux_newclass(lua_State *L, const char *classname, luaL_reg *func)
{
luaL_newmetatable(L, classname); /* mt */
/* set __tostring metamethod */
lua_pushstring(L, "__tostring");
lua_pushcfunction(L, aux_meth_tostring);
lua_rawset(L, -3);
/* create __index table to place methods */
lua_pushstring(L, "__index"); /* mt,"__index" */
lua_newtable(L); /* mt,"__index",it */
luaL_openlib(L, NULL, func, 0);
/* put class name into class metatable */
lua_pushstring(L, "class"); /* mt,"__index",it,"class" */
lua_pushstring(L, classname); /* mt,"__index",it,"class",classname */
lua_rawset(L, -3); /* mt,"__index",it */
/* get __gc method from class and use it for garbage collection */
lua_pushstring(L, "__gc"); /* mt,"__index",it,"__gc" */
lua_pushstring(L, "__gc"); /* mt,"__index",it,"__gc","__gc" */
lua_rawget(L, -3); /* mt,"__index",it,"__gc",fn */
lua_rawset(L, -5); /* mt,"__index",it */
/* pass all methods that start with _ to the metatable, and all others
* to the index table */
for (; func->name; func++) { /* mt,"__index",it */
lua_pushstring(L, func->name);
lua_pushcfunction(L, func->func);
lua_rawset(L, func->name[0] == '_' ? -5: -3);
}
lua_rawset(L, -3); /* mt */
lua_pop(L, 1);
}

View file

@ -30,474 +30,153 @@ EMAIL = "anonymous@anonymous.org"
BLOCKSIZE = 2048
-----------------------------------------------------------------------------
-- Gets ip and port for data connection from PASV answer
-- Input
-- pasv: PASV command answer
-- Returns
-- ip: string containing ip for data connection
-- port: port for data connection
-- Low level FTP API
-----------------------------------------------------------------------------
local function get_pasv(pasv)
local a, b, c, d, p1, p2, _
local ip, port
_,_, a, b, c, d, p1, p2 =
string.find(pasv, "(%d*),(%d*),(%d*),(%d*),(%d*),(%d*)")
if not (a and b and c and d and p1 and p2) then return nil, nil end
ip = string.format("%d.%d.%d.%d", a, b, c, d)
port = tonumber(p1)*256 + tonumber(p2)
return ip, port
local metat = { __index = {} }
function open(server, port)
local tp = socket.try(socket.tp.connect(server, port or PORT))
return setmetatable({tp = tp}, metat)
end
-----------------------------------------------------------------------------
-- Check server greeting
-- Input
-- control: control connection with server
-- Returns
-- code: nil if error
-- answer: server answer or error message
-----------------------------------------------------------------------------
local function greet(control)
local code, answer = check_answer(control, {120, 220})
if code == 120 then -- please try again, somewhat busy now...
return check_answer(control, {220})
end
return code, answer
local function port(portt)
return portt.server:accept()
end
-----------------------------------------------------------------------------
-- Log in on server
-- Input
-- control: control connection with server
-- user: user name
-- password: user password if any
-- Returns
-- code: nil if error
-- answer: server answer or error message
-----------------------------------------------------------------------------
local function login(control, user, password)
local code, answer = command(control, "user", user, {230, 331})
if code == 331 and password then -- need pass and we have pass
return command(control, "pass", password, {230, 202})
end
return code, answer
local function pasv(pasvt)
return socket.connect(pasvt.ip, pasvt.port)
end
-----------------------------------------------------------------------------
-- Change to target directory
-- Input
-- control: socket for control connection with server
-- path: directory to change to
-- Returns
-- code: nil if error
-- answer: server answer or error message
-----------------------------------------------------------------------------
local function cwd(control, path)
function metat.__index:login(user, password)
socket.try(self.tp:command("USER", user))
local code, reply = socket.try(self.tp:check{"2..", 331})
if code == 331 then
socket.try(password, reply)
socket.try(self.tp:command("PASS", password))
socket.try(self.tp:check("2.."))
end
return 1
end
-----------------------------------------------------------------------------
-- Change to target directory
-- Input
-- control: socket for control connection with server
-- Returns
-- server: server socket bound to local address, nil if error
-- answer: error message if any
-----------------------------------------------------------------------------
local function port(control)
local code, answer
local server, ctl_ip
ctl_ip, answer = control:getsockname()
server, answer = socket.bind(ctl_ip, 0)
server:settimeout(TIMEOUT)
local ip, p, ph, pl
ip, p = server:getsockname()
pl = math.mod(p, 256)
ph = (p - pl)/256
function metat.__index:pasv()
socket.try(self.tp:command("PASV"))
local code, reply = socket.try(self.tp:check("2.."))
local _, _, a, b, c, d, p1, p2 =
string.find(reply, "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)")
socket.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.portt then
self.portt.server:close()
self.portt = nil
end
return self.pasvt.ip, self.pasvt.port
end
function metat.__index:port(ip, port)
self.pasvt = nil
local server
if not ip then
ip, port = socket.try(self.tp:getcontrol():getsockname())
server = socket.try(socket.bind(ip, 0))
ip, port = socket.try(server:getsockname())
socket.try(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), "%.", ",")
code, answer = command(control, "port", arg, {200})
if not code then
server:close()
return nil, answer
else return server end
socket.try(self.tp:command("port", arg))
socket.try(self.tp:check("2.."))
self.portt = server and {ip = ip, port = port, server = server}
return 1
end
-----------------------------------------------------------------------------
-- Closes control connection with server
-- Input
-- control: control connection with server
-- Returns
-- code: nil if error
-- answer: server answer or error message
-----------------------------------------------------------------------------
local function logout(control)
local code, answer = command(control, "quit", nil, {221})
if code then control:close() end
return code, answer
end
-----------------------------------------------------------------------------
-- Receives data and send it to a callback
-- Input
-- data: data connection
-- callback: callback to return file contents
-- Returns
-- nil if successfull, or an error message in case of error
-----------------------------------------------------------------------------
local function receive_indirect(data, callback)
local chunk, err, res
while not err do
chunk, err = try_receive(data, BLOCKSIZE)
if err == "closed" then err = "done" end
res = callback(chunk, err)
if not res then break end
end
end
-----------------------------------------------------------------------------
-- Retrieves file or directory listing
-- Input
-- control: control connection with server
-- server: server socket bound to local address
-- name: file name
-- is_directory: is file a directory name?
-- content_cb: callback to receive file contents
-- Returns
-- err: error message in case of error, nil otherwise
-----------------------------------------------------------------------------
local function retrieve(control, server, name, is_directory, content_cb)
local code, answer
local data
-- ask server for file or directory listing accordingly
if is_directory then
code, answer = cwd(control, name)
if not code then return answer end
code, answer = command(control, "nlst", nil, {150, 125})
else
code, answer = command(control, "retr", name, {150, 125})
end
if not code then return nil, answer end
data, answer = server:accept()
server:close()
if not data then
control:close()
return answer
end
answer = receive_indirect(data, content_cb)
if answer then
control:close()
return answer
end
data:close()
-- make sure file transfered ok
return check_answer(control, {226, 250})
end
-----------------------------------------------------------------------------
-- Stores a file
-- Input
-- control: control connection with server
-- server: server socket bound to local address
-- file: file name under current directory
-- send_cb: callback to produce the file contents
-- Returns
-- code: return code, nil if error
-- answer: server answer or error message
-----------------------------------------------------------------------------
local function store(control, server, file, send_cb)
local data, err
local code, answer = command(control, "stor", file, {150, 125})
if not code then
control:close()
return nil, answer
end
-- start data connection
data, answer = server:accept()
server:close()
if not data then
control:close()
return nil, answer
end
-- send whole file
err = send_indirect(data, send_cb, send_cb())
if err then
control:close()
return nil, err
end
-- close connection to inform that file transmission is complete
data:close()
-- check if file was received correctly
return check_answer(control, {226, 250})
end
-----------------------------------------------------------------------------
-- Change transfer type
-- Input
-- control: control connection with server
-- params: "type=i" for binary or "type=a" for ascii
-- Returns
-- err: error message if any
-----------------------------------------------------------------------------
local function change_type(control, params)
local type, _
_, _, type = string.find(params or "", "type=(.)")
if type == "a" or type == "i" then
local code, err = command(control, "type", type, {200})
if not code then return err end
end
end
-----------------------------------------------------------------------------
-- Starts a control connection, checks the greeting and log on
-- Input
-- parsed: parsed URL components
-- Returns
-- control: control connection with server, or nil if error
-- err: error message if any
-----------------------------------------------------------------------------
local function open(parsed)
local control, err = socket.tp.connect(parsed.host, parsed.port)
if not control then return nil, err end
function metat.__index:send(sendt)
local data
socket.try(self.pasvt or self.portt, "need port or pasv first")
if self.pasvt then data = socket.try(pasv(self.pasvt)) end
socket.try(self.tp:command(sendt.command, sendt.argument))
if self.portt then data = socket.try(port(self.portt)) end
local step = sendt.step or ltn12.pump.step
local code, reply
-- greet
code, reply = control:check({120, 220})
if code == 120 then -- busy, try again
code, reply = control:check(220)
local checkstep = function(src, snk)
local readyt = socket.select(readt, nil, 0)
if readyt[tp] then
code, reply = self.tp:check{"2..", "1.."}
if not code then
data:close()
return nil, reply
end
end
local ret, err = step(src, snk)
if err then data:close() end
return ret, err
end
-- authenticate
code, reply = control:command("user", user)
code, reply = control:check({230, 331})
if code == 331 and password then -- need pass and we have pass
control:command("pass", password)
code, reply = control:check({230, 202})
local sink = socket.sink("close-when-empty", data)
socket.try(ltn12.pump.all(sendt.source, sink, checkstep))
if not code then code = socket.try(self.tp:check{"1..", "2.."}) end
if string.find(code, "1..") then socket.try(self.tp:check("2..")) end
return 1
end
function metat.__index:receive(recvt)
local data
socket.try(self.pasvt or self.portt, "need port or pasv first")
if self.pasvt then data = socket.try(pasv(self.pasvt)) end
socket.try(self.tp:command(recvt.command, recvt.argument))
if self.portt then data = socket.try(port(self.portt)) end
local source = socket.source("until-closed", data)
local step = recvt.step or ltn12.pump.step
local checkstep = function(src, snk)
local ret, err = step(src, snk)
if err then data:close() end
return ret, err
end
-- change directory
local segment = parse_path(parsed)
for i, v in ipairs(segment) do
code, reply = control:command("cwd")
code, reply = control:check(250)
end
-- change type
local type = string.sub(params or "", 7, 7)
if type == "a" or type == "i" then
code, reply = control:command("type", type)
code, reply = control:check(200)
end
socket.try(ltn12.pump.all(source, recvt.sink, checkstep))
local code = socket.try(self.tp:check{"1..", "2.."})
if string.find(code, "1..") then socket.try(self.tp:check("2..")) end
return 1
end
return change_dir(control, segment) or
change_type(control, parsed.params) or
download(control, request, segment) or
close(control)
function metat.__index:cwd(dir)
socket.try(self.tp:command("CWD", dir))
socket.try(self.tp:check(250))
return 1
end
function metat.__index:type(type)
socket.try(self.tp:command("TYPE", type))
socket.try(self.tp:check(200))
return 1
end
function metat.__index:greet()
local code = socket.try(self.tp:check{"1..", "2.."})
if string.find(code, "1..") then socket.try(self.tp:check("2..")) end
return 1
end
function metat.__index:quit()
socket.try(self.tp:command("QUIT"))
socket.try(self.tp:check("2.."))
return 1
end
function metat.__index:close()
socket.try(self.tp:close())
return 1
end
-----------------------------------------------------------------------------
-- Stores a file in current directory
-- Input
-- control: control connection with server
-- request: a table with the fields:
-- content_cb: send callback to send file contents
-- segment: parsed URL path segments
-- Returns
-- err: error message if any
-- High level FTP API
-----------------------------------------------------------------------------
local function upload(control, request, segment)
local code, name, content_cb
-- get remote file name
name = segment[table.getn(segment)]
if not name then
control:close()
return "Invalid file path"
end
content_cb = request.content_cb
-- setup passive connection
local server, answer = port(control)
if not server then return answer end
-- ask server to receive file
code, answer = store(control, server, name, content_cb)
if not code then return answer end
function put(putt)
end
-----------------------------------------------------------------------------
-- Download a file from current directory
-- Input
-- control: control connection with server
-- request: a table with the fields:
-- content_cb: receive callback to receive file contents
-- segment: parsed URL path segments
-- Returns
-- err: error message if any
-----------------------------------------------------------------------------
local function download(control, request, segment)
local code, name, is_directory, content_cb
is_directory = segment.is_directory
content_cb = request.content_cb
-- get remote file name
name = segment[table.getn(segment)]
if not name and not is_directory then
control:close()
return "Invalid file path"
end
-- setup passive connection
local server, answer = port(control)
if not server then return answer end
-- ask server to send file or directory listing
code, answer = retrieve(control, server, name,
is_directory, content_cb)
if not code then return answer end
function get(gett)
end
-----------------------------------------------------------------------------
-- Parses the FTP URL setting default values
-- Input
-- request: a table with the fields:
-- url: the target URL
-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
-- user: account user name
-- password: account password
-- Returns
-- parsed: a table with parsed components
-----------------------------------------------------------------------------
local function parse_url(request)
local parsed = socket.url.parse(request.url, {
user = "anonymous",
port = 21,
path = "/",
password = EMAIL,
scheme = "ftp"
})
-- explicit login information overrides that given by URL
parsed.user = request.user or parsed.user
parsed.password = request.password or parsed.password
-- explicit representation type overrides that given by URL
if request.type then parsed.params = "type=" .. request.type end
return parsed
end
-----------------------------------------------------------------------------
-- Parses the FTP URL path setting default values
-- Input
-- parsed: a table with the parsed URL components
-- Returns
-- dirs: a table with parsed directory components
-----------------------------------------------------------------------------
local function parse_path(parsed_url)
local segment = socket.url.parse_path(parsed_url.path)
segment.is_directory = segment.is_directory or
(parsed_url.params == "type=d")
return segment
end
-----------------------------------------------------------------------------
-- Builds a request table from a URL or request table
-- Input
-- url_or_request: target url or request table (a table with the fields:
-- url: the target URL
-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
-- user: account user name
-- password: account password)
-- Returns
-- request: request table
-----------------------------------------------------------------------------
local function build_request(data)
local request = {}
if type(data) == "table" then for i, v in data do request[i] = v end
else request.url = data end
return request
end
-----------------------------------------------------------------------------
-- Downloads a file from a FTP server
-- Input
-- request: a table with the fields:
-- url: the target URL
-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
-- user: account user name
-- password: account password
-- content_cb: receive callback to receive file contents
-- Returns
-- err: error message if any
-----------------------------------------------------------------------------
function get_cb(request)
local parsed = parse_url(request)
if parsed.scheme ~= "ftp" then
return string.format("unknown scheme '%s'", parsed.scheme)
end
local control, err = open(parsed)
if not control then return err end
local segment = parse_path(parsed)
return change_dir(control, segment) or
change_type(control, parsed.params) or
download(control, request, segment) or
close(control)
end
-----------------------------------------------------------------------------
-- Uploads a file to a FTP server
-- Input
-- request: a table with the fields:
-- url: the target URL
-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
-- user: account user name
-- password: account password
-- content_cb: send callback to send file contents
-- Returns
-- err: error message if any
-----------------------------------------------------------------------------
function put_cb(request)
local parsed = parse_url(request)
if parsed.scheme ~= "ftp" then
return string.format("unknown scheme '%s'", parsed.scheme)
end
local control, err = open(parsed)
if not control then return err end
local segment = parse_path(parsed)
err = change_dir(control, segment) or
change_type(control, parsed.params) or
upload(control, request, segment) or
close(control)
if err then return nil, err
else return 1 end
end
-----------------------------------------------------------------------------
-- Uploads a file to a FTP server
-- Input
-- url_or_request: target url or request table (a table with the fields:
-- url: the target URL
-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
-- user: account user name
-- password: account password)
-- content: file contents
-- content: file contents
-- Returns
-- err: error message if any
-----------------------------------------------------------------------------
function put(url_or_request, content)
local request = build_request(url_or_request)
request.content = request.content or content
request.content_cb = socket.callback.send_string(request.content)
return put_cb(request)
end
-----------------------------------------------------------------------------
-- Retrieve a file from a ftp server
-- Input
-- url_or_request: target url or request table (a table with the fields:
-- url: the target URL
-- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory
-- user: account user name
-- password: account password)
-- Returns
-- data: file contents as a string
-- err: error message in case of error, nil otherwise
-----------------------------------------------------------------------------
function get(url_or_request)
local concat = socket.concat.create()
local request = build_request(url_or_request)
request.content_cb = socket.callback.receive_concat(concat)
local err = get_cb(request)
return concat:getresult(), err
end
return socket.ftp
return ftp

View file

@ -68,7 +68,7 @@ end
local function receive_body(reqt, respt, tmp)
local sink = reqt.sink or ltn12.sink.null()
local pump = reqt.pump or ltn12.pump
local step = reqt.step or ltn12.pump.step
local source
local te = respt.headers["transfer-encoding"]
if te and te ~= "identity" then
@ -80,9 +80,9 @@ local function receive_body(reqt, respt, tmp)
source = socket.source("by-length", tmp.sock, length)
else
-- get it all until connection closes
source = socket.source("until-closed", tmp.sock)
source = socket.source(tmp.sock)
end
socket.try(pump(source, sink))
socket.try(ltn12.pump.all(source, sink, step))
end
local function send_headers(sock, headers)
@ -125,7 +125,7 @@ end
local function send_request(reqt, respt, tmp)
local uri = request_uri(reqt, respt, tmp)
local headers = tmp.headers
local pump = reqt.pump or ltn12.pump
local step = reqt.step or ltn12.pump.step
-- send request line
socket.try(tmp.sock:send((reqt.method or "GET")
.. " " .. uri .. " HTTP/1.1\r\n"))
@ -136,9 +136,11 @@ local function send_request(reqt, respt, tmp)
-- send request message body, if any
if not reqt.source then return end
if headers["content-length"] then
socket.try(pump(reqt.source, socket.sink(tmp.sock)))
socket.try(ltn12.pump.all(reqt.source,
socket.sink(tmp.sock), step))
else
socket.try(pump(reqt.source, socket.sink("http-chunked", tmp.sock)))
socket.try(ltn12.pump.all(reqt.source,
socket.sink("http-chunked", tmp.sock), step))
end
end

View file

@ -8,6 +8,7 @@ setfenv(1, ltn12)
filter = {}
source = {}
sink = {}
pump = {}
-- 2048 seems to be better in windows...
BLOCKSIZE = 2048
@ -22,7 +23,6 @@ end
-- returns a high level filter that cycles a cycles a low-level filter
function filter.cycle(low, ctx, extra)
if type(low) ~= 'function' then error('invalid low-level filter', 2) end
return function(chunk)
local ret
ret, ctx = low(ctx, chunk, extra)
@ -32,8 +32,6 @@ end
-- chains two filters together
local function chain2(f1, f2)
if type(f1) ~= 'function' then error('invalid filter', 2) end
if type(f2) ~= 'function' then error('invalid filter', 2) end
local co = coroutine.create(function(chunk)
while true do
local filtered1 = f1(chunk)
@ -58,7 +56,6 @@ end
function filter.chain(...)
local f = arg[1]
for i = 2, table.getn(arg) do
if type(arg[i]) ~= 'function' then error('invalid filter', 2) end
f = chain2(f, arg[i])
end
return f
@ -93,7 +90,6 @@ end
-- turns a fancy source into a simple source
function source.simplify(src)
if type(src) ~= 'function' then error('invalid source', 2) end
return function()
local chunk, err_or_new = src()
src = err_or_new or src
@ -117,7 +113,6 @@ end
-- creates rewindable source
function source.rewind(src)
if type(src) ~= 'function' then error('invalid source', 2) end
local t = {}
return function(chunk)
if not chunk then
@ -132,8 +127,6 @@ end
-- chains a source with a filter
function source.chain(src, f)
if type(src) ~= 'function' then error('invalid source', 2) end
if type(f) ~= 'function' then error('invalid filter', 2) end
local co = coroutine.create(function()
while true do
local chunk, err = src()
@ -152,20 +145,21 @@ function source.chain(src, f)
end
end
-- creates a source that produces contents of several files one after the
-- creates a source that produces contents of several sources, one after the
-- other, as if they were concatenated
function source.cat(...)
local co = coroutine.create(function()
local i = 1
while i <= table.getn(arg) do
local chunk = arg[i]:read(2048)
while i <= table.getn(arg) do
local chunk, err = arg[i]()
if chunk then coroutine.yield(chunk)
else i = i + 1 end
elseif err then return nil, err
else i = i + 1 end
end
end)
return source.simplify(function()
return function()
return shift(coroutine.resume(co))
end)
end
end
-- creates a sink that stores into a table
@ -180,7 +174,6 @@ end
-- turns a fancy sink into a simple sink
function sink.simplify(snk)
if type(snk) ~= 'function' then error('invalid sink', 2) end
return function(chunk, err)
local ret, err_or_new = snk(chunk, err)
if not ret then return nil, err_or_new end
@ -219,8 +212,6 @@ end
-- chains a sink with a filter
function sink.chain(f, snk)
if type(snk) ~= 'function' then error('invalid sink', 2) end
if type(f) ~= 'function' then error('invalid filter', 2) end
return function(chunk, err)
local filtered = f(chunk)
local done = chunk and ""
@ -233,15 +224,18 @@ function sink.chain(f, snk)
end
end
-- pumps all data from a source to a sink
function pump(src, snk)
if type(src) ~= 'function' then error('invalid source', 2) end
if type(snk) ~= 'function' then error('invalid sink', 2) end
-- 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)
return chunk and ret and not src_err and not snk_err, src_err or snk_err
end
-- pumps all data from a source to a sink, using a step function
function pump.all(src, snk, step)
step = step or pump.step
while true do
local chunk, src_err = src()
local ret, snk_err = snk(chunk, src_err)
if not chunk or not ret then
return not src_err and not snk_err, src_err or snk_err
end
local ret, err = step(src, snk)
if not ret then return not err, err end
end
end

View file

@ -25,6 +25,7 @@
\*=========================================================================*/
#include "luasocket.h"
#include "base.h"
#include "auxiliar.h"
#include "timeout.h"
#include "buffer.h"
@ -39,34 +40,8 @@
/*=========================================================================*\
* Declarations
\*=========================================================================*/
static int base_open(lua_State *L);
static int mod_open(lua_State *L, const luaL_reg *mod);
/*-------------------------------------------------------------------------*\
* Setup basic stuff.
\*-------------------------------------------------------------------------*/
static int base_open(lua_State *L)
{
/* create namespace table */
lua_pushstring(L, LUASOCKET_LIBNAME);
lua_newtable(L);
#ifdef LUASOCKET_DEBUG
lua_pushstring(L, "debug");
lua_pushnumber(L, 1);
lua_rawset(L, -3);
#endif
/* make version string available so scripts */
lua_pushstring(L, "version");
lua_pushstring(L, LUASOCKET_VERSION);
lua_rawset(L, -3);
/* store namespace as global */
lua_settable(L, LUA_GLOBALSINDEX);
/* make sure modules know what is our namespace */
lua_pushstring(L, "LUASOCKET_LIBNAME");
lua_pushstring(L, LUASOCKET_LIBNAME);
lua_settable(L, LUA_GLOBALSINDEX);
return 0;
}
static int mod_open(lua_State *L, const luaL_reg *mod)
{
@ -79,6 +54,7 @@ static int mod_open(lua_State *L, const luaL_reg *mod)
#include "tp.lch"
#include "smtp.lch"
#include "http.lch"
#include "ftp.lch"
#else
lua_dofile(L, "ltn12.lua");
lua_dofile(L, "auxiliar.lua");
@ -87,6 +63,7 @@ static int mod_open(lua_State *L, const luaL_reg *mod)
lua_dofile(L, "tp.lua");
lua_dofile(L, "smtp.lua");
lua_dofile(L, "http.lua");
lua_dofile(L, "ftp.lua");
#endif
return 0;
}

View file

@ -14,14 +14,9 @@
/*=========================================================================*\
* Don't want to trust escape character constants
\*=========================================================================*/
#define CR 0x0D
#define LF 0x0A
#define HT 0x09
#define SP 0x20
typedef unsigned char UC;
static const char CRLF[] = {CR, LF, 0};
static const char EQCRLF[] = {'=', CR, LF, 0};
static const char CRLF[] = "\r\n";
static const char EQCRLF[] = "=\r\n";
/*=========================================================================*\
* Internal function prototypes.
@ -121,9 +116,9 @@ static int mime_global_wrp(lua_State *L)
luaL_buffinit(L, &buffer);
while (input < last) {
switch (*input) {
case CR:
case '\r':
break;
case LF:
case '\n':
luaL_addstring(&buffer, CRLF);
left = length;
break;
@ -327,11 +322,10 @@ static int mime_global_unb64(lua_State *L)
* all (except CRLF in text) can be =XX
* CLRL in not text must be =XX=XX
* 33 through 60 inclusive can be plain
* 62 through 120 inclusive can be plain
* 62 through 126 inclusive can be plain
* 9 and 32 can be plain, unless in the end of a line, where must be =XX
* encoded lines must be no longer than 76 not counting CRLF
* soft line-break are =CRLF
* !"#$@[\]^`{|}~ should be =XX for EBCDIC compatibility
* To encode one byte, we need to see the next two.
* Worst case is when we see a space, and wonder if a CRLF is comming
\*-------------------------------------------------------------------------*/
@ -344,16 +338,10 @@ static void qpsetup(UC *qpclass, UC *qpunbase)
int i;
for (i = 0; i < 256; i++) qpclass[i] = QP_QUOTED;
for (i = 33; i <= 60; i++) qpclass[i] = QP_PLAIN;
for (i = 62; i <= 120; i++) qpclass[i] = QP_PLAIN;
qpclass[HT] = QP_IF_LAST; qpclass[SP] = QP_IF_LAST;
qpclass['!'] = QP_QUOTED; qpclass['"'] = QP_QUOTED;
qpclass['#'] = QP_QUOTED; qpclass['$'] = QP_QUOTED;
qpclass['@'] = QP_QUOTED; qpclass['['] = QP_QUOTED;
qpclass['\\'] = QP_QUOTED; qpclass[']'] = QP_QUOTED;
qpclass['^'] = QP_QUOTED; qpclass['`'] = QP_QUOTED;
qpclass['{'] = QP_QUOTED; qpclass['|'] = QP_QUOTED;
qpclass['}'] = QP_QUOTED; qpclass['~'] = QP_QUOTED;
qpclass['}'] = QP_QUOTED; qpclass[CR] = QP_CR;
for (i = 62; i <= 126; i++) qpclass[i] = QP_PLAIN;
qpclass['\t'] = QP_IF_LAST;
qpclass[' '] = QP_IF_LAST;
qpclass['\r'] = QP_CR;
for (i = 0; i < 256; i++) qpunbase[i] = 255;
qpunbase['0'] = 0; qpunbase['1'] = 1; qpunbase['2'] = 2;
qpunbase['3'] = 3; qpunbase['4'] = 4; qpunbase['5'] = 5;
@ -377,7 +365,7 @@ static void qpquote(UC c, luaL_Buffer *buffer)
/*-------------------------------------------------------------------------*\
* Accumulate characters until we are sure about how to deal with them.
* Once we are sure, output the to the buffer, in the correct form.
* Once we are sure, output to the buffer, in the correct form.
\*-------------------------------------------------------------------------*/
static size_t qpencode(UC c, UC *input, size_t size,
const char *marker, luaL_Buffer *buffer)
@ -389,7 +377,7 @@ static size_t qpencode(UC c, UC *input, size_t size,
/* might be the CR of a CRLF sequence */
case QP_CR:
if (size < 2) return size;
if (input[1] == LF) {
if (input[1] == '\n') {
luaL_addstring(buffer, marker);
return 0;
} else qpquote(input[0], buffer);
@ -398,7 +386,7 @@ static size_t qpencode(UC c, UC *input, size_t size,
case QP_IF_LAST:
if (size < 3) return size;
/* if it is the last, quote it and we are done */
if (input[1] == CR && input[2] == LF) {
if (input[1] == '\r' && input[2] == '\n') {
qpquote(input[0], buffer);
luaL_addstring(buffer, marker);
return 0;
@ -492,19 +480,19 @@ static size_t qpdecode(UC c, UC *input, size_t size,
case '=':
if (size < 3) return size;
/* eliminate soft line break */
if (input[1] == CR && input[2] == LF) return 0;
if (input[1] == '\r' && input[2] == '\n') return 0;
/* decode quoted representation */
c = qpunbase[input[1]]; d = qpunbase[input[2]];
/* if it is an invalid, do not decode */
if (c > 15 || d > 15) luaL_addlstring(buffer, (char *)input, 3);
else luaL_putchar(buffer, (c << 4) + d);
return 0;
case CR:
case '\r':
if (size < 2) return size;
if (input[1] == LF) luaL_addlstring(buffer, (char *)input, 2);
if (input[1] == '\n') luaL_addlstring(buffer, (char *)input, 2);
return 0;
default:
if (input[0] == HT || (input[0] > 31 && input[0] < 127))
if (input[0] == '\t' || (input[0] > 31 && input[0] < 127))
luaL_putchar(buffer, input[0]);
return 0;
}
@ -582,9 +570,9 @@ static int mime_global_qpwrp(lua_State *L)
luaL_buffinit(L, &buffer);
while (input < last) {
switch (*input) {
case CR:
case '\r':
break;
case LF:
case '\n':
left = length;
luaL_addstring(&buffer, CRLF);
break;
@ -623,7 +611,7 @@ static int mime_global_qpwrp(lua_State *L)
* c is the current character being processed
* last is the previous character
\*-------------------------------------------------------------------------*/
#define eolcandidate(c) (c == CR || c == LF)
#define eolcandidate(c) (c == '\r' || c == '\n')
static int eolprocess(int c, int last, const char *marker,
luaL_Buffer *buffer)
{

View file

@ -21,7 +21,6 @@ static int meth_set(lua_State *L);
static int meth_isset(lua_State *L);
static int c_select(lua_State *L);
static int global_select(lua_State *L);
static void check_obj_tab(lua_State *L, int tabidx);
/* fd_set object methods */
static luaL_reg set[] = {
@ -68,9 +67,6 @@ static int global_select(lua_State *L)
fd_set *read_fd_set, *write_fd_set;
/* make sure we have enough arguments (nil is the default) */
lua_settop(L, 3);
/* check object tables */
check_obj_tab(L, 1);
check_obj_tab(L, 2);
/* check timeout */
if (!lua_isnil(L, 3) && !lua_isnumber(L, 3))
luaL_argerror(L, 3, "number or nil expected");
@ -127,24 +123,3 @@ static int c_select(lua_State *L)
timeout < 0 ? NULL : &tv));
return 1;
}
static void check_obj_tab(lua_State *L, int tabidx)
{
if (tabidx < 0) tabidx = lua_gettop(L) + tabidx + 1;
if (lua_istable(L, tabidx)) {
lua_pushnil(L);
while (lua_next(L, tabidx) != 0) {
if (aux_getgroupudata(L, "select{able}", -1) == NULL) {
char msg[45];
if (lua_isnumber(L, -2))
sprintf(msg, "table entry #%g is invalid",
lua_tonumber(L, -2));
else
sprintf(msg, "invalid entry found in table");
luaL_argerror(L, tabidx, msg);
}
lua_pop(L, 1);
}
} else if (!lua_isnil(L, tabidx))
luaL_argerror(L, tabidx, "table or nil expected");
}

View file

@ -20,6 +20,7 @@ DOMAIN = os.getenv("SERVER_NAME") or "localhost"
-- default time zone (means we don't know)
ZONE = "-0000"
local function shift(a, b, c)
return b, c
end
@ -29,31 +30,66 @@ function stuff()
return ltn12.filter.cycle(dot, 2)
end
-- send message or throw an exception
local function send_p(control, mailt)
socket.try(control:check("2.."))
socket.try(control:command("EHLO", mailt.domain or DOMAIN))
socket.try(control:check("2.."))
socket.try(control:command("MAIL", "FROM:" .. mailt.from))
socket.try(control:check("2.."))
if type(mailt.rcpt) == "table" then
for i,v in ipairs(mailt.rcpt) do
socket.try(control:command("RCPT", "TO:" .. v))
socket.try(control:check("2.."))
end
else
socket.try(control:command("RCPT", "TO:" .. mailt.rcpt))
socket.try(control:check("2.."))
end
socket.try(control:command("DATA"))
socket.try(control:check("3.."))
socket.try(control:source(ltn12.source.chain(mailt.source, stuff())))
socket.try(control:send("\r\n.\r\n"))
socket.try(control:check("2.."))
socket.try(control:command("QUIT"))
socket.try(control:check("2.."))
---------------------------------------------------------------------------
-- Low level SMTP API
-----------------------------------------------------------------------------
local metat = { __index = {} }
function metat.__index:greet(domain)
socket.try(self.tp:check("2.."))
socket.try(self.tp:command("EHLO", domain or DOMAIN))
return socket.try(self.tp:check("2.."))
end
function metat.__index:mail(from)
socket.try(self.tp:command("MAIL", "FROM:" .. from))
return socket.try(self.tp:check("2.."))
end
function metat.__index:rcpt(to)
socket.try(self.tp:command("RCPT", "TO:" .. to))
return socket.try(self.tp:check("2.."))
end
function metat.__index:data(src)
socket.try(self.tp:command("DATA"))
socket.try(self.tp:check("3.."))
socket.try(self.tp:source(src))
socket.try(self.tp:send("\r\n.\r\n"))
return socket.try(self.tp:check("2.."))
end
function metat.__index:quit()
socket.try(self.tp:command("QUIT"))
return socket.try(self.tp:check("2.."))
end
function metat.__index:close()
return socket.try(self.tp:close())
end
-- send message or throw an exception
function metat.__index:send(mailt)
self:mail(mailt.from)
if type(mailt.rcpt) == "table" then
for i,v in ipairs(mailt.rcpt) do
self:rcpt(v)
end
else
self:rcpt(mailt.rcpt)
end
self:data(ltn12.source.chain(mailt.source, stuff()))
end
function open(server, port)
local tp, error = socket.tp.connect(server or SERVER, port or PORT)
if not tp then return nil, error end
return setmetatable({tp = tp}, metat)
end
---------------------------------------------------------------------------
-- Multipart message source
-----------------------------------------------------------------------------
-- returns a hopefully unique mime boundary
local seqno = 0
local function newboundary()
@ -147,13 +183,17 @@ function message(mesgt)
return function() return shift(coroutine.resume(co)) end
end
function send(mailt)
local c, e = socket.tp.connect(mailt.server or SERVER, mailt.port or PORT)
if not c then return nil, e end
local s, e = pcall(send_p, c, mailt)
c:close()
if s then return true
else return nil, e end
end
---------------------------------------------------------------------------
-- High level SMTP API
-----------------------------------------------------------------------------
send = socket.protect(function(mailt)
local server = mailt.server or SERVER
local port = mailt.port or PORT
local smtp = socket.try(open(server, port))
smtp:greet(mailt.domain or DOMAIN)
smtp:send(mailt)
smtp:quit()
return smtp:close()
end)
return smtp

View file

@ -15,6 +15,7 @@
#include "socket.h"
#include "inet.h"
#include "options.h"
#include "base.h"
#include "tcp.h"
/*=========================================================================*\
@ -40,6 +41,7 @@ static int meth_dirty(lua_State *L);
/* tcp object methods */
static luaL_reg tcp[] = {
{"__gc", meth_close},
{"__tostring", base_meth_tostring},
{"accept", meth_accept},
{"bind", meth_bind},
{"close", meth_close},
@ -58,7 +60,6 @@ static luaL_reg tcp[] = {
{"settimeout", meth_settimeout},
{"shutdown", meth_shutdown},
{NULL, NULL}
};
/* socket option handlers */

View file

@ -18,22 +18,19 @@ setfenv(1, socket.tp)
TIMEOUT = 60
-- gets server reply
local function get_reply(sock)
-- gets server reply (works for SMTP and FTP)
local function get_reply(control)
local code, current, separator, _
local line, err = sock:receive()
local line, err = control:receive()
local reply = line
if err then return nil, err end
_, _, code, separator = string.find(line, "^(%d%d%d)(.?)")
if not code then return nil, "invalid server reply" end
if separator == "-" then -- reply is multiline
repeat
line, err = sock:receive()
line, err = control:receive()
if err then return nil, err end
_,_, current, separator = string.find(line, "^(%d%d%d)(.)")
if not current or not separator then
return nil, "invalid server reply"
end
_,_, current, separator = string.find(line, "^(%d%d%d)(.?)")
reply = reply .. "\n" .. line
-- reply ends with same code
until code == current and separator == " "
@ -42,60 +39,73 @@ local function get_reply(sock)
end
-- metatable for sock object
local metatable = { __index = {} }
local metat = { __index = {} }
function metatable.__index:check(ok)
local code, reply = get_reply(self.sock)
function metat.__index:check(ok)
local code, reply = get_reply(self.control)
if not code then return nil, reply end
if type(ok) ~= "function" then
if type(ok) == "table" then
for i, v in ipairs(ok) do
if string.find(code, v) then return code, reply end
if string.find(code, v) then return tonumber(code), reply end
end
return nil, reply
else
if string.find(code, ok) then return code, reply
if string.find(code, ok) then return tonumber(code), reply
else return nil, reply end
end
else return ok(code, reply) end
else return ok(tonumber(code), reply) end
end
function metatable.__index:command(cmd, arg)
if arg then return self.sock:send(cmd .. " " .. arg.. "\r\n")
else return self.sock:send(cmd .. "\r\n") end
function metat.__index:command(cmd, arg)
if arg then return self.control:send(cmd .. " " .. arg.. "\r\n")
else return self.control:send(cmd .. "\r\n") end
end
function metatable.__index:sink(snk, pat)
local chunk, err = sock:receive(pat)
function metat.__index:sink(snk, pat)
local chunk, err = control:receive(pat)
return snk(chunk, err)
end
function metatable.__index:send(data)
return self.sock:send(data)
function metat.__index:send(data)
return self.control:send(data)
end
function metatable.__index:receive(pat)
return self.sock:receive(pat)
function metat.__index:receive(pat)
return self.control:receive(pat)
end
function metatable.__index:source(src, instr)
function metat.__index:getfd()
return self.control:getfd()
end
function metat.__index:dirty()
return self.control:dirty()
end
function metat.__index:getcontrol()
return self.control
end
function metat.__index:source(src, instr)
while true do
local chunk, err = src()
if not chunk then return not err, err end
local ret, err = self.sock:send(chunk)
local ret, err = self.control:send(chunk)
if not ret then return nil, err end
end
end
-- closes the underlying sock
function metatable.__index:close()
self.sock:close()
-- closes the underlying control
function metat.__index:close()
self.control:close()
return 1
end
-- connect with server and return sock object
-- connect with server and return control object
function connect(host, port)
local sock, err = socket.connect(host, port)
if not sock then return nil, err end
sock:settimeout(TIMEOUT)
return setmetatable({sock = sock}, metatable)
local control, err = socket.connect(host, port)
if not control then return nil, err end
control:settimeout(TIMEOUT)
return setmetatable({control = control}, metat)
end

View file

@ -15,6 +15,7 @@
#include "socket.h"
#include "inet.h"
#include "options.h"
#include "base.h"
#include "udp.h"
/*=========================================================================*\
@ -50,6 +51,7 @@ static luaL_reg udp[] = {
{"close", meth_close},
{"setoption", meth_setoption},
{"__gc", meth_close},
{"__tostring", base_meth_tostring},
{"getfd", meth_getfd},
{"setfd", meth_setfd},
{"dirty", meth_dirty},