From 17c4d1c30544f0ed638879835f179ada96249868 Mon Sep 17 00:00:00 2001 From: Diego Nehab Date: Fri, 29 Dec 2000 22:15:09 +0000 Subject: [PATCH] Initial revision --- README | 24 +++ etc/dict.lua | 39 ++++ makefile.dist | 81 ++++++++ samples/README | 32 ++++ samples/listener.lua | 25 +++ samples/talker.lua | 22 +++ src/ftp.lua | 437 +++++++++++++++++++++++++++++++++++++++++++ src/http.lua | 312 ++++++++++++++++++++++++++++++ src/luasocket.h | 18 ++ src/smtp.lua | 338 +++++++++++++++++++++++++++++++++ test/testclnt.lua | 359 +++++++++++++++++++++++++++++++++++ test/testsrvr.lua | 90 +++++++++ 12 files changed, 1777 insertions(+) create mode 100644 README create mode 100644 etc/dict.lua create mode 100644 makefile.dist create mode 100644 samples/README create mode 100644 samples/listener.lua create mode 100644 samples/talker.lua create mode 100644 src/ftp.lua create mode 100644 src/http.lua create mode 100644 src/luasocket.h create mode 100644 src/smtp.lua create mode 100644 test/testclnt.lua create mode 100644 test/testsrvr.lua diff --git a/README b/README new file mode 100644 index 0000000..c4ac6fd --- /dev/null +++ b/README @@ -0,0 +1,24 @@ +This directory contains the implementation of the protocols FTP, HTTP and +SMTP. The files provided are: + + http.lua -- HTTP protocol implementation + base64.lua -- base64 encoding implementation + +The module http.lua provides functionality to download an URL from a +HTTP server. The implementation conforms to the HTTP/1.1 standard, RFC +2068. The base64.lua module provides base64 encoding and decoding. The +module is used for the HTTP Basic Authentication Scheme, and conforms to +RFC 1521. + + smtp.lua -- SMTP protocol implementation + +The module smtp.lua provides functionality to send e-mail messages to a +SMTP mail server. The implementation conforms to RFC 821. + + ftp.lua -- FTP protocol implementation + +The module ftp.lua provides functions to download and upload files from +and to FTP servers. The implementation conforms to RFC 959. + +These implementations are supported. Please send any comments do +diego@tecgraf.puc-rio.br. diff --git a/etc/dict.lua b/etc/dict.lua new file mode 100644 index 0000000..683cb45 --- /dev/null +++ b/etc/dict.lua @@ -0,0 +1,39 @@ +-- dict.lua +-- simple client for DICT protocol (see http://www.dict.org/) +-- shows definitions for each word from stdin. uses only "wn" dictionary. +-- if a word is "=", then the rest of the line is sent verbatim as a protocol +-- command to the server. + +if verbose then verbose=write else verbose=function()end end + +verbose(">>> connecting to server\n") +local s,e=connect("dict.org",2628) +assert(s,e) +verbose(">>> connected\n") + +while 1 do + local w=read"*w" + if w==nil then break end + if w=="=" then + w=read"*l" + verbose(">>>",w,"\n") + send(s,w,"\r\n") + else + verbose(">>> looking up `",w,"'\n") + send(s,"DEFINE wn ",w,"\r\n") + end + while 1 do + local l=receive(s) + if l==nil then break end + if strfind(l,"^[0-9]") then + write("<<< ",l,"\n") + else + write(l,"\n") + end + if strfind(l,"^250") or strfind(l,"^[45]") then break end + end +end + +send(s,"QUIT\r\n") +verbose("<<< ",receive(s),"\n") +close(s) diff --git a/makefile.dist b/makefile.dist new file mode 100644 index 0000000..19a3775 --- /dev/null +++ b/makefile.dist @@ -0,0 +1,81 @@ +#-------------------------------------------------------------------------- +# LuaSocket makefile +# Test executable for socklib +# Diego Nehab, 29/8/1999 +#-------------------------------------------------------------------------- + +# don't echo commands +# .SILENT: + +CXX = g++ +CC = gcc + +DIST = luasocket-1.1 + +WARNINGS = -Wall -Wshadow -Wpointer-arith -Waggregate-return -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -Wnested-externs + +CFLAGS = $(WARNINGS) -D_DEBUG -O2 + +LUA = /home/diego/lib/lua +LUALIB = $(LUA)/lib +LUAINC = $(LUA)/include + +INC = -I$(LUAINC) +# LIB = $(LUA_LIB)/liblualib.a $(LUA_LIB)/liblua.a -lm -lsocket -lnsl +# LIB = $(LUA_LIB)/liblualib.a $(LUA_LIB)/liblua.a -lm -lnsl +LIB = $(LUALIB)/liblualib.a $(LUALIB)/liblua.a -lm + +SRC = ~diego/tec/luasocket +OBJ = /tmp +DEP = /tmp + +# list of .cpp files +c_sources = luasocket.c lua.c + +# corresponding .o files +c_objects = $(addprefix $(OBJ)/, $(addsuffix .o, $(basename $(c_sources)))) + +# binary depends on objects +luasocket: $(c_objects) + $(CC) $(CPPFLAGS) -o $@ $(c_objects) $(LIB) + +# rule to create them +$(c_objects): $(OBJ)/%.o: $(SRC)/%.c + $(CC) -c $(CFLAGS) $(INC) -o $@ $< + +# corresponding .d files +c_deps = $(addprefix $(DEP)/, $(addsuffix .d, $(basename $(c_sources)))) + +# makefile depend on them... +makefile : $(c_deps) + +# ... since it includes them +-include $(c_deps) + +# rule to create them +$(c_deps) : $(DEP)/%.d : $(SRC)/%.c + $(SHELL) -ec '$(CC) -MM $(CFLAGS) $(INC) $< \ + | sed '\''s%\($*\.o\)%$@ $(OBJ)/\1%'\'' > $@; \ + [ -s $@ ] || rm -f $@' + +# clean all trash +clean: + rm -f $(OBJ)/*.o + rm -f $(DEP)/*.d + rm -f luasocket core + +dist: + mkdir -p $(DIST)/lua + mkdir -p $(DIST)/examples + mkdir -p $(DIST)/html + cp -vf *.c $(DIST) + cp -vf *.h $(DIST) + cp -vf makefile $(DIST) + cp -vf README $(DIST) + cp -vf lua/*.lua $(DIST)/lua + cp -vf lua/README $(DIST)/lua + cp -vf examples/*.lua $(DIST)/examples + cp -vf examples/README $(DIST)/examples + cp -vf html/manual.html $(DIST)/html + tar -zcvf $(DIST).tar.gz $(DIST) + zip -r $(DIST).zip $(DIST) diff --git a/samples/README b/samples/README new file mode 100644 index 0000000..ef5017b --- /dev/null +++ b/samples/README @@ -0,0 +1,32 @@ +This directory contains some sample programs using LuaSocket as well as +the automatic tests used to make sure the library is working properly. + +The files provided are: + + server.lua -- test server + client.lua -- test client + command.lua -- test command definitions + +The automatic tests are composed by three files: client.lua, command.lua +and server.lua. To run the automatic tests on your system, make sure to +compile the library with _DEBUG defined (check makefile) and then open +two terminals. Run 'luasocket server.lua' on one of them and 'luasocket +client.lua' on the other. The programs should start talking to each +other. + + listen.lua -- echo server + talk.lua -- echo tester + +listen.lua and talk.lua are about the simplest applications you can +write using LuaSocket. Run 'luasocket listen.lua' and 'luasocket +talk.lua' on different terminals. Whatever you type on talk.lua will be +printed by listen.lua. + + dict.lua -- dict client + +The dict.lua module is a cool simple client for the DICT protocol, +written by Luiz Henrique Figueiredo. Just run it and enter a few words +to see it working. + +Good luck, +Diego. diff --git a/samples/listener.lua b/samples/listener.lua new file mode 100644 index 0000000..a47d9a3 --- /dev/null +++ b/samples/listener.lua @@ -0,0 +1,25 @@ +host = "localhost" +port = 8080 +if arg then + host = arg[1] or host + port = arg[2] or port +end +print("Binding to host '" ..host.. "' and port " ..port.. "...") +s, i, p, e = bind(host, port) +if not s then + print(e) + exit() +end +print("Waiting connection from talker on " .. i .. ":" .. p .. "...") +c, e = s:accept() +if not c then + print(e) + exit() +end +print("Connected. Here is the stuff:") +l, e = c:receive() +while not e do + print(l) + l, e = c:receive() +end +print(e) diff --git a/samples/talker.lua b/samples/talker.lua new file mode 100644 index 0000000..b3313e6 --- /dev/null +++ b/samples/talker.lua @@ -0,0 +1,22 @@ +host = "localhost" +port = 8080 +if arg then + host = arg[1] or host + port = arg[2] or port +end +print("Attempting connection to host '" ..host.. "' and port " ..port.. "...") +c, e = connect(host, port) +if not c then + print(e) + exit() +end +print("Connected! Please type stuff (empty line to stop):") +l = read() +while l and l ~= "" and not e do + e = c:send(l, "\n") + if e then + print(e) + exit() + end + l = read() +end diff --git a/src/ftp.lua b/src/ftp.lua new file mode 100644 index 0000000..b817356 --- /dev/null +++ b/src/ftp.lua @@ -0,0 +1,437 @@ +----------------------------------------------------------------------------- +-- Simple FTP support for the Lua language using the LuaSocket toolkit. +-- Author: Diego Nehab +-- Date: 26/12/2000 +-- Conforming to: RFC 959 +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout in seconds before the program gives up on a connection +local TIMEOUT = 60 +-- default port for ftp service +local PORT = 21 +-- this is the default anonymous password. used when no password is +-- provided in url. should be changed for your e-mail. +local EMAIL = "anonymous@anonymous.org" + +----------------------------------------------------------------------------- +-- Parses a url and returns its scheme, user, password, host, port +-- and path components, according to RFC 1738, Uniform Resource Locators (URL), +-- of December 1994 +-- Input +-- url: unique resource locator desired +-- default: table containing default values to be returned +-- Returns +-- table with the following fields: +-- host: host to connect +-- path: url path +-- port: host port to connect +-- user: user name +-- pass: password +-- scheme: protocol +----------------------------------------------------------------------------- +local split_url = function(url, default) + -- initialize default parameters + local parsed = default or {} + -- get scheme + url = gsub(url, "^(.+)://", function (s) %parsed.scheme = s end) + -- get user name and password. both can be empty! + -- moreover, password can be ommited + url = gsub(url, "^([^@:/]*)(:?)([^:@/]-)@", function (u, c, p) + %parsed.user = u + -- there can be an empty password, but the ':' has to be there + -- or else there is no password + %parsed.pass = nil -- kill default password + if c == ":" then %parsed.pass = p end + end) + -- get host + url = gsub(url, "^([%w%.%-]+)", function (h) %parsed.host = h end) + -- get port if any + url = gsub(url, "^:(%d+)", function (p) %parsed.port = p end) + -- whatever is left is the path + if url ~= "" then parsed.path = url end + return parsed +end + +----------------------------------------------------------------------------- +-- 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 +----------------------------------------------------------------------------- +local get_pasv = function(pasv) + local a,b,c,d,p1,p2 + local ip, port + _,_, a, b, c, d, p1, p2 = + strfind(pasv, "(%d*),(%d*),(%d*),(%d*),(%d*),(%d*)") + if not a or not b or not c or not d or not p1 or not p2 then + return nil, nil + end + ip = format("%d.%d.%d.%d", a, b, c, d) + port = tonumber(p1)*256 + tonumber(p2) + return ip, port +end + +----------------------------------------------------------------------------- +-- Sends a FTP command through socket +-- Input +-- control: control connection socket +-- cmd: command +-- arg: command argument if any +----------------------------------------------------------------------------- +local send_command = function(control, cmd, arg) + local line, err + if arg then line = cmd .. " " .. arg .. "\r\n" + else line = cmd .. "\r\n" end + err = control:send(line) + return err +end + +----------------------------------------------------------------------------- +-- Gets FTP command answer, unfolding if neccessary +-- Input +-- control: control connection socket +-- Returns +-- answer: whole server reply, nil if error +-- code: answer status code or error message +----------------------------------------------------------------------------- +local get_answer = function(control) + local code, lastcode, sep + local line, err = control:receive() + local answer = line + if err then return nil, err end + _,_, code, sep = strfind(line, "^(%d%d%d)(.)") + if not code or not sep then return nil, answer end + if sep == "-" then -- answer is multiline + repeat + line, err = control:receive() + if err then return nil, err end + _,_, lastcode, sep = strfind(line, "^(%d%d%d)(.)") + answer = answer .. "\n" .. line + until code == lastcode and sep == " " -- answer ends with same code + end + return answer, tonumber(code) +end + +----------------------------------------------------------------------------- +-- Checks if a message return is correct. Closes control connection if not. +-- Input +-- control: control connection socket +-- success: table with successfull reply status code +-- Returns +-- code: reply code or nil in case of error +-- answer: server complete answer or system error message +----------------------------------------------------------------------------- +local check_answer = function(control, success) + local answer, code = %get_answer(control) + if not answer then + control:close() + return nil, code + end + if type(success) ~= "table" then success = {success} end + for i = 1, getn(success) do + if code == success[i] then + return code, answer + end + end + control:close() + return nil, answer +end + +----------------------------------------------------------------------------- +-- Trys a command on control socked, in case of error, the control connection +-- is closed. +-- Input +-- control: control connection socket +-- cmd: command +-- arg: command argument or nil if no argument +-- success: table with successfull reply status code +-- Returns +-- code: reply code or nil in case of error +-- answer: server complete answer or system error message +----------------------------------------------------------------------------- +local try_command = function(control, cmd, arg, success) + local err = %send_command(control, cmd, arg) + if err then + control:close() + return nil, err + end + local code, answer = %check_answer(control, success) + if not code then return nil, answer end + return code, answer +end + +----------------------------------------------------------------------------- +-- Creates a table with all directories in path +-- Input +-- file: abolute path to file +-- Returns +-- file: filename +-- path: table with directories to reach filename +-- isdir: is it a directory or a file +----------------------------------------------------------------------------- +local split_path = function(file) + local path = {} + local isdir + file = file or "/" + -- directory ends with a '/' + _,_, isdir = strfind(file, "([/])$") + gsub(file, "([^/]+)", function (dir) tinsert(%path, dir) end) + if not isdir then file = tremove(path) + else file = nil end + return file, path, isdir +end + +----------------------------------------------------------------------------- +-- Check server greeting +-- Input +-- control: control connection with server +-- Returns +-- code: nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local check_greeting = function(control) + local code, answer = %check_answer(control, {120, 220}) + if not code then return nil, answer end + if code == 120 then -- please try again, somewhat busy now... + code, answer = %check_answer(control, {220}) + end + return code, answer +end + +----------------------------------------------------------------------------- +-- Log in on server +-- Input +-- control: control connection with server +-- user: user name +-- pass: user password if any +-- Returns +-- code: nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local login = function(control, user, pass) + local code, answer = %try_command(control, "user", parsed.user, {230, 331}) + if not code then return nil, answer end + if code == 331 and parsed.pass then -- need pass and we have pass + code, answer = %try_command(control, "pass", parsed.pass, {230, 202}) + end + return code, answer +end + +----------------------------------------------------------------------------- +-- Change to target directory +-- Input +-- control: socket for control connection with server +-- path: array with directories in order +-- Returns +-- code: nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local cwd = function(control, path) + local code, answer = 250, "Home directory used" + for i = 1, getn(path) do + code, answer = %try_command(control, "cwd", path[i], {250}) + if not code then return nil, answer end + end + return code, answer +end + +----------------------------------------------------------------------------- +-- Start data connection with server +-- Input +-- control: control connection with server +-- Returns +-- data: socket for data connection with server, nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local start_dataconnection = function(control) + -- ask for passive data connection + local code, answer = %try_command(control, "pasv", nil, {227}) + if not code then return nil, answer end + -- get data connection parameters from server reply + local host, port = %get_pasv(answer) + if not host or not port then return nil, answer end + -- start data connection with given parameters + local data, err = connect(host, port) + if not data then return nil, err end + data:timeout(%TIMEOUT) + return data +end + +----------------------------------------------------------------------------- +-- Closes control connection with server +-- Input +-- control: control connection with server +-- Returns +-- code: nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local logout = function(control) + local code, answer = %try_command(control, "quit", nil, {221}) + if not code then return nil, answer end + control:close() + return code, answer +end + +----------------------------------------------------------------------------- +-- Retrieves file or directory listing +-- Input +-- control: control connection with server +-- data: data connection with server +-- file: file name under current directory +-- isdir: is file a directory name? +-- Returns +-- file: string with file contents, nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local retrieve_file = function(control, data, file, isdir) + -- ask server for file or directory listing accordingly + if isdir then code, answer = %try_command(control, "nlst", file, {150, 125}) + else code, answer = %try_command(control, "retr", file, {150, 125}) end + if not code then + control:close() + data:close() + return nil, answer + end + -- download whole file + file, err = data:receive("*a") + data:close() + if err then + control:close() + return nil, err + end + -- make sure file transfered ok + code, answer = %check_answer(control, {226, 250}) + if not code then return nil, answer + else return file, answer end +end + +----------------------------------------------------------------------------- +-- Stores a file +-- Input +-- control: control connection with server +-- data: data connection with server +-- file: file name under current directory +-- bytes: file contents in string +-- Returns +-- file: string with file contents, nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local store_file = function (control, data, file, bytes) + local code, answer = %try_command(control, "stor", file, {150, 125}) + if not code then + data:close() + return nil, answer + end + -- send whole file and close connection to mark file end + answer = data:send(bytes) + data:close() + if answer then + control:close() + return nil, answer + end + -- check if file was received right + return %check_answer(control, {226, 250}) +end + +----------------------------------------------------------------------------- +-- Change transfer type +-- Input +-- control: control connection with server +-- type: new transfer type +-- Returns +-- code: nil if error +-- answer: server answer or error message +----------------------------------------------------------------------------- +local change_type = function(control, type) + if type == "b" then type = "i" else type = "a" end + return %try_command(control, "type", type, {200}) +end + +----------------------------------------------------------------------------- +-- Retrieve a file from a ftp server +-- Input +-- url: file location +-- type: "binary" or "ascii" +-- Returns +-- file: downloaded file or nil in case of error +-- err: error message if any +----------------------------------------------------------------------------- +function ftp_get(url, type) + local control, data, err + local answer, code, server, file, path + parsed = %split_url(url, {user = "anonymous", port = 21, pass = %EMAIL}) + -- start control connection + control, err = connect(parsed.host, parsed.port) + if not control then return nil, err end + control:timeout(%TIMEOUT) + -- get and check greeting + code, answer = %check_greeting(control) + if not code then return nil, answer end + -- try to log in + code, answer = %login(control, parsed.user, parsed.pass) + if not code then return nil, answer end + -- go to directory + file, path, isdir = %split_path(parsed.path) + code, answer = %cwd(control, path) + if not code then return nil, answer end + -- change to binary type? + code, answer = %change_type(control, type) + if not code then return nil, answer end + -- start data connection + data, answer = %start_dataconnection(control) + if not data then return nil, answer end + -- ask server to send file or directory listing + file, answer = %retrieve_file(control, data, file, isdir) + if not file then return nil, answer end + -- disconnect + %logout(control) + -- return whatever file we received plus a possible error + return file, answer +end + +----------------------------------------------------------------------------- +-- Uploads a file to a FTP server +-- Input +-- url: file location +-- bytes: file contents +-- type: "binary" or "ascii" +-- Returns +-- err: error message if any +----------------------------------------------------------------------------- +function ftp_put(url, bytes, type) + local control, data + local answer, code, server, file, path + parsed = %split_url(url, {user = "anonymous", port = 21, pass = %EMAIL}) + -- start control connection + control, answer = connect(parsed.host, parsed.port) + if not control then return answer end + control:timeout(%TIMEOUT) + -- get and check greeting + code, answer = %check_greeting(control) + if not code then return answer end + -- try to log in + code, answer = %login(control, parsed.user, parsed.pass) + if not code then return answer end + -- go to directory + file, path, isdir = %split_path(parsed.path) + code, answer = %cwd(control, path) + if not code then return answer end + -- change to binary type? + code, answer = %change_type(control, type) + if not code then return answer end + -- start data connection + data, answer = %start_dataconnection(control) + if not data then return answer end + -- ask server to send file or directory listing + code, answer = %store_file(control, data, file, bytes) + if not code then return answer end + -- disconnect + %logout(control) + -- return whatever file we received plus a possible error + return nil +end diff --git a/src/http.lua b/src/http.lua new file mode 100644 index 0000000..8f08725 --- /dev/null +++ b/src/http.lua @@ -0,0 +1,312 @@ +----------------------------------------------------------------------------- +-- Simple HTTP/1.1 support for the Lua language using the LuaSocket toolkit. +-- Author: Diego Nehab +-- Date: 26/12/2000 +-- Conforming to: RFC 2068 +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- connection timeout in seconds +local TIMEOUT = 60 +-- default port for document retrieval +local PORT = 80 +-- user agent field sent in request +local USERAGENT = "LuaSocket/HTTP 1.0" + +----------------------------------------------------------------------------- +-- Tries to get a line from the server or close socket if error +-- sock: socket connected to the server +-- Returns +-- line: line received or nil in case of error +-- err: error message if any +----------------------------------------------------------------------------- +local try_getline = function(sock) + line, err = sock:receive() + if err then + sock:close() + return nil, err + end + return line +end + +----------------------------------------------------------------------------- +-- Tries to send a line to the server or close socket if error +-- sock: socket connected to the server +-- line: line to send +-- Returns +-- err: error message if any +----------------------------------------------------------------------------- +local try_sendline = function(sock, line) + err = sock:send(line) + if err then sock:close() end + return err +end + +----------------------------------------------------------------------------- +-- Retrieves status from http reply +-- Input +-- reply: http reply string +-- Returns +-- status: integer with status code +----------------------------------------------------------------------------- +local get_status = function(reply) + local _,_, status = strfind(reply, " (%d%d%d) ") + return tonumber(status) +end + +----------------------------------------------------------------------------- +-- Receive server reply messages +-- Input +-- sock: server socket +-- Returns +-- status: server reply status code or nil if error +-- reply: full server reply +-- err: error message if any +----------------------------------------------------------------------------- +local get_reply = function(sock) + local reply, err + reply, err = %try_getline(sock) + if not err then return %get_status(reply), reply + else return nil, nil, err end +end + +----------------------------------------------------------------------------- +-- Receive and parse mime headers +-- Input +-- sock: server socket +-- mime: a table that might already contain mime headers +-- Returns +-- mime: a table with all mime headers in the form +-- {name_1 = "value_1", name_2 = "value_2" ... name_n = "value_n"} +-- all name_i are lowercase +-- nil and error message in case of error +----------------------------------------------------------------------------- +local get_mime = function(sock, mime) + local line, err + local name, value + -- get first line + line, err = %try_getline(sock) + 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 = strfind(line, "(.-):%s*(.*)") + name = strlower(name) + -- get next line (value might be folded) + line, err = %try_getline(sock) + if err then return nil, err end + -- unfold any folded values + while not err and line ~= "" and (strsub(line, 1, 1) == " ") do + value = value .. line + line, err = %try_getline(sock) + if err then return nil, err end + end + -- save pair in table + if mime[name] then + -- join any multiple field + mime[name] = mime[name] .. ", " .. value + else + -- new field + mime[name] = value + end + end + return mime +end + +----------------------------------------------------------------------------- +-- Receives http body +-- Input +-- sock: server socket +-- mime: initial mime headers +-- Returns +-- body: a string containing the body of the document +-- nil and error message in case of error +-- Obs: +-- mime: headers might be modified by chunked transfer +----------------------------------------------------------------------------- +local get_body = function(sock, mime) + local body, err + if mime["transfer-encoding"] == "chunked" then + local chunk_size, line + body = "" + repeat + -- get chunk size, skip extention + line, err = %try_getline(sock) + if err then return nil, err end + chunk_size = tonumber(gsub(line, ";.*", ""), 16) + if not chunk_size then + sock:close() + return nil, "invalid chunk size" + end + -- get chunk + line, err = sock:receive(chunk_size) + if err then + sock:close() + return nil, err + end + -- concatenate new chunk + body = body .. line + -- skip blank line + _, err = %try_getline(sock) + if err then return nil, err end + until chunk_size <= 0 + -- store extra mime headers + --_, err = %get_mime(sock, mime) + --if err then return nil, err end + elseif mime["content-length"] then + body, err = sock:receive(tonumber(mime["content-length"])) + if err then + sock:close() + return nil, err + end + else + -- get it all until connection closes! + body, err = sock:receive("*a") + if err then + sock:close() + return nil, err + end + end + -- return whole body + return body +end + +----------------------------------------------------------------------------- +-- Parses a url and returns its scheme, user, password, host, port +-- and path components, according to RFC 1738, Uniform Resource Locators (URL), +-- of December 1994 +-- Input +-- url: unique resource locator desired +-- default: table containing default values to be returned +-- Returns +-- table with the following fields: +-- host: host to connect +-- path: url path +-- port: host port to connect +-- user: user name +-- pass: password +-- scheme: protocol +----------------------------------------------------------------------------- +local split_url = function(url, default) + -- initialize default parameters + local parsed = default or {} + -- get scheme + url = gsub(url, "^(.+)://", function (s) %parsed.scheme = s end) + -- get user name and password. both can be empty! + -- moreover, password can be ommited + url = gsub(url, "^([^@:/]*)(:?)([^:@/]-)@", function (u, c, p) + %parsed.user = u + -- there can be an empty password, but the ':' has to be there + -- or else there is no password + %parsed.pass = nil -- kill default password + if c == ":" then %parsed.pass = p end + end) + -- get host + url = gsub(url, "^([%w%.%-]+)", function (h) %parsed.host = h end) + -- get port if any + url = gsub(url, "^:(%d+)", function (p) %parsed.port = p end) + -- whatever is left is the path + if url ~= "" then parsed.path = url end + return parsed +end + +----------------------------------------------------------------------------- +-- Sends a GET message through socket +-- Input +-- socket: http connection socket +-- path: path requested +-- mime: mime headers to send in request +-- Returns +-- err: nil in case of success, error message otherwise +----------------------------------------------------------------------------- +local send_get = function(sock, path, mime) + local err = %try_sendline(sock, "GET " .. path .. " HTTP/1.1\r\n") + if err then return err end + for i, v in mime do + err = %try_sendline(sock, i .. ": " .. v .. "\r\n") + if err then return err end + end + err = %try_sendline(sock, "\r\n") + return err +end + +----------------------------------------------------------------------------- +-- Converts field names to lowercase +-- Input +-- headers: user header fields +-- parsed: parsed url components +-- Returns +-- mime: a table with the same headers, but with lowercase field names +----------------------------------------------------------------------------- +local fill_headers = function(headers, parsed) + local mime = {} + headers = headers or {} + for i,v in headers do + mime[strlower(i)] = v + end + mime["connection"] = "close" + mime["host"] = parsed.host + mime["user-agent"] = %USERAGENT + if parsed.user and parsed.pass then -- Basic Authentication + mime["authorization"] = "Basic ".. + base64(parsed.user .. ":" .. parsed.pass) + end + return mime +end + +----------------------------------------------------------------------------- +-- We need base64 convertion routines for Basic Authentication Scheme +----------------------------------------------------------------------------- +dofile("base64.lua") + +----------------------------------------------------------------------------- +-- Downloads and receives a http url, with its mime headers +-- Input +-- url: unique resource locator desired +-- headers: headers to send with request +-- tried: is this an authentication retry? +-- Returns +-- body: document body, if successfull +-- mime: headers received with document, if sucessfull +-- reply: server reply, if successfull +-- err: error message, if any +----------------------------------------------------------------------------- +function http_get(url, headers) + local sock, err, mime, body, status, reply + -- get url components + local parsed = %split_url(url, {port = %PORT, path ="/"}) + -- fill default headers + headers = %fill_headers(headers, parsed) + -- try connection + sock, err = connect(parsed.host, parsed.port) + if not sock then return nil, nil, nil, err end + -- set connection timeout + sock:timeout(%TIMEOUT) + -- send request + err = %send_get(sock, parsed.path, headers) + if err then return nil, nil, nil, err end + -- get server message + status, reply, err = %get_reply(sock) + if err then return nil, nil, nil, err end + -- get url accordingly + if status == 200 then -- ok, go on and get it + mime, err = %get_mime(sock, {}) + if err then return nil, nil, reply, err end + body, err = %get_body(sock, mime) + if err then return nil, mime, reply, err end + sock:close() + return body, mime, reply + elseif status == 301 then -- moved permanently, try again + mime = %get_mime(sock, {}) + sock:close() + if mime["location"] then return http_get(mime["location"], headers) + else return nil, mime, reply end + elseif status == 401 then + mime, err = %get_mime(sock, {}) + if err then return nil, nil, reply, err end + return nil, mime, reply + end + return nil, nil, reply +end diff --git a/src/luasocket.h b/src/luasocket.h new file mode 100644 index 0000000..d4037cd --- /dev/null +++ b/src/luasocket.h @@ -0,0 +1,18 @@ +/*=========================================================================*\ +* TCP/IP support for LUA +* Diego Nehab +* 9/11/1999 +\*=========================================================================*/ + +#ifndef _LUASOCKET_H_ +#define _LUASOCKET_H_ + +/*=========================================================================*\ +* Exported function declarations +\*=========================================================================*/ +/*-------------------------------------------------------------------------*\ +* Initializes toolkit +\*-------------------------------------------------------------------------*/ +void lua_socketlibopen(lua_State *L); + +#endif /* _LUASOCKET_H_ */ diff --git a/src/smtp.lua b/src/smtp.lua new file mode 100644 index 0000000..f9ed64c --- /dev/null +++ b/src/smtp.lua @@ -0,0 +1,338 @@ +----------------------------------------------------------------------------- +-- Simple SMTP support for the Lua language using the LuaSocket toolkit. +-- Author: Diego Nehab +-- Date: 26/12/2000 +-- Conforming to: RFC 821 +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout in secconds before we give up waiting +local TIMEOUT = 180 +-- port used for connection +local PORT = 25 +-- domain used in HELO command. If we are under a CGI, try to get from +-- environment +local DOMAIN = getenv("SERVER_NAME") +if not DOMAIN then + DOMAIN = "localhost" +end + +----------------------------------------------------------------------------- +-- Tries to send DOS mode lines. Closes socket on error. +-- Input +-- sock: server socket +-- line: string to be sent +-- Returns +-- err: message in case of error, nil if successfull +----------------------------------------------------------------------------- +local puts = function(sock, line) + local err = sock:send(line .. "\r\n") + if err then sock:close() end + return err +end + +----------------------------------------------------------------------------- +-- Tries to receive DOS mode lines. Closes socket on error. +-- Input +-- sock: server socket +-- Returns +-- line: received string if successfull, nil in case of error +-- err: error message if any +----------------------------------------------------------------------------- +local gets = function(sock) + local line, err = sock:receive("*l") + if err then + sock:close() + return nil, err + end + return line +end + +----------------------------------------------------------------------------- +-- Gets a reply from the server and close connection if it is wrong +-- Input +-- sock: server socket +-- accept: acceptable errorcodes +-- Returns +-- code: server reply code. nil if error +-- line: complete server reply message or error message +----------------------------------------------------------------------------- +local get_reply = function(sock, accept) + local line, err = %gets(sock) + if line then + if type(accept) ~= "table" then accept = {accept} end + local _,_, code = strfind(line, "^(%d%d%d)") + if not code then return nil, line end + code = tonumber(code) + for i = 1, getn(accept) do + if code == accept[i] then return code, line end + end + sock:close() + return nil, line + end + return nil, err +end + +----------------------------------------------------------------------------- +-- Sends a command to the server +-- Input +-- sock: server socket +-- command: command to be sent +-- param: command parameters if any +-- Returns +-- err: error message if any +----------------------------------------------------------------------------- +local send_command = function(sock, command, param) + local line + if param then line = command .. " " .. param + else line = command end + return %puts(sock, line) +end + +----------------------------------------------------------------------------- +-- Gets the initial server greeting +-- Input +-- sock: server socket +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local get_helo = function(sock) + return %get_reply(sock, 220) +end + +----------------------------------------------------------------------------- +-- Sends initial client greeting +-- Input +-- sock: server socket +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local send_helo = function(sock) + local err = %send_command(sock, "HELO", %DOMAIN) + if not err then + return %get_reply(sock, 250) + else return nil, err end +end + +----------------------------------------------------------------------------- +-- Sends mime headers +-- Input +-- sock: server socket +-- mime: table with mime headers to be sent +-- Returns +-- err: error message if any +----------------------------------------------------------------------------- +local send_mime = function(sock, mime) + local err + mime = mime or {} + -- send all headers + for name,value in mime do + err = sock:send(name .. ": " .. value .. "\r\n") + if err then + sock:close() + return err + end + end + -- end mime part + err = sock:send("\r\n") + if err then sock:close() end + return err +end + +----------------------------------------------------------------------------- +-- Sends connection termination command +-- Input +-- sock: server socket +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local send_quit = function(sock) + local code, answer + local err = %send_command(sock, "QUIT") + if not err then + code, answer = %get_reply(sock, 221) + sock:close() + return code, answer + else return nil, err end +end + +----------------------------------------------------------------------------- +-- Sends sender command +-- Input +-- sock: server socket +-- sender: e-mail of sender +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local send_mail = function(sock, sender) + local param = format("FROM:<%s>", sender) + local err = %send_command(sock, "MAIL", param) + if not err then + return %get_reply(sock, 250) + else return nil, err end +end + +----------------------------------------------------------------------------- +-- Sends message mime headers and body +-- Input +-- sock: server socket +-- mime: table containing all mime headers to be sent +-- body: message body +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local send_data = function (sock, mime, body) + local err = %send_command(sock, "DATA") + if not err then + local code, answer = %get_reply(sock, 354) + if not code then return nil, answer end + -- avoid premature end in message body + body = gsub(body or "", "\n%.", "\n%.%.") + -- mark end of message body + body = body .. "\r\n." + err = %send_mime(sock, mime) + if err then return nil, err end + err = %puts(sock, body) + return %get_reply(sock, 250) + else return nil, err end +end + +----------------------------------------------------------------------------- +-- Sends recipient list command +-- Input +-- sock: server socket +-- rcpt: lua table with recipient list +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local send_rcpt = function(sock, rcpt) + local err, code, answer + if type(rcpt) ~= "table" then rcpt = {rcpt} end + for i = 1, getn(rcpt) do + err = %send_command(sock, "RCPT", format("TO:<%s>", rcpt[i])) + if not err then + code, answer = %get_reply(sock, {250, 251}) + if not code then return code, answer end + else return nil, err end + end + return code, answer +end + +----------------------------------------------------------------------------- +-- Sends verify recipient command +-- Input +-- sock: server socket +-- user: user to be verified +-- Returns +-- code: server status code, nil if error +-- answer: complete server reply +----------------------------------------------------------------------------- +local send_vrfy = function (sock, user) + local err = %send_command(sock, "VRFY", format("<%s>", user)) + if not err then + return %get_reply(sock, {250, 251}) + else return nil, err end +end + +----------------------------------------------------------------------------- +-- Connection oriented mail functions +----------------------------------------------------------------------------- +function smtp_connect(server) + local code, answer + -- connect to server + local sock, err = connect(server, %PORT) + if not sock then return nil, err end + sock:timeout(%TIMEOUT) + -- initial server greeting + code, answer = %get_helo(sock) + if not code then return nil, answer end + -- HELO + code, answer = %send_helo(sock) + if not code then return nil, answer end + return sock +end + +function smtp_send(sock, from, rcpt, mime, body) + local code, answer + -- MAIL + code, answer = %send_mail(sock, from) + if not code then return nil, answer end + -- RCPT + code, answer = %send_rcpt(sock, rcpt) + if not code then return nil, answer end + -- DATA + return %send_data(sock, mime, body) +end + +function smtp_close(sock) + -- QUIT + return %send_quit(sock) +end + +----------------------------------------------------------------------------- +-- Main mail function +-- Input +-- from: message sender +-- rcpt: table containing message recipients +-- mime: table containing mime headers +-- body: message body +-- server: smtp server to be used +-- Returns +-- nil if successfull, error message in case of error +----------------------------------------------------------------------------- +function smtp_mail(from, rcpt, mime, body, server) + local sock, err = smtp_connect(server) + if not sock then return err end + local code, answer = smtp_send(sock, from, rcpt, mime, body) + if not code then return answer end + code, answer = smtp_close(sock) + if not code then return answer + else return nil end +end + +--=========================================================================== +-- Compatibility functions +--=========================================================================== +----------------------------------------------------------------------------- +-- Converts a comma separated list into a Lua table with one entry for each +-- list element. +-- Input +-- str: string containing the list to be converted +-- tab: table to be filled with entries +-- Returns +-- a table t, where t.n is the number of elements with an entry t[i] +-- for each element +----------------------------------------------------------------------------- +local fill = function(str, tab) + gsub(str, "([^%s,]+)", function (w) tinsert(%tab, w) end) + return tab +end + +----------------------------------------------------------------------------- +-- Client mail function, implementing CGILUA 3.2 interface +----------------------------------------------------------------------------- +function mail(msg) + local rcpt = {} + local mime = {} + mime["Subject"] = msg.subject + mime["To"] = msg.to + mime["From"] = msg.from + %fill(msg.to, rcpt) + if msg.cc then + %fill(msg.cc, rcpt) + mime["Cc"] = msg.cc + end + if msg.bcc then + %fill(msg.bcc, rcpt) + end + rcpt.n = nil + return %smtp_mail(msg.from, rcpt, mime, msg.message, msg.mailserver) +end diff --git a/test/testclnt.lua b/test/testclnt.lua new file mode 100644 index 0000000..c1c22bd --- /dev/null +++ b/test/testclnt.lua @@ -0,0 +1,359 @@ +----------------------------------------------------------------------------- +-- LuaSocket automated test module +-- client.lua +-- This is the client module. It connects with the server module and executes +-- all tests. +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Prints a header to separate the test phases +-- Input +-- test: test phase name +----------------------------------------------------------------------------- +function new_test(test) + write("----------------------------------------------\n", + test, "\n", + "----------------------------------------------\n") +end + +----------------------------------------------------------------------------- +-- Read command definitions and stablish control connection +----------------------------------------------------------------------------- +new_test("initializing...") +dofile("command.lua") +test_debug_mode() +while control == nil do + print("client: trying control connection...") + control, err = connect(HOST, PORT) + if control then + print("client: control connection stablished!") + else + sleep(2) + end +end + +----------------------------------------------------------------------------- +-- Make sure server is ready for data transmission +----------------------------------------------------------------------------- +function sync() + send_command(SYNC) + get_command() +end + +----------------------------------------------------------------------------- +-- Close and reopen data connection, to get rid of any unread blocks +----------------------------------------------------------------------------- +function reconnect() + if data then + data:close() + send_command(CLOSE) + data = nil + end + while data == nil do + send_command(CONNECT) + data = connect(HOST, PORT) + if not data then + print("client: waiting for data connection.") + sleep(1) + end + end + sync() +end + +----------------------------------------------------------------------------- +-- Tests the command connection +----------------------------------------------------------------------------- +function test_command(cmd, par) + local cmd_back, par_back + reconnect() + send_command(COMMAND) + write("testing command ") + print_command(cmd, par) + send_command(cmd, par) + cmd_back, par_back = get_command() + if cmd_back ~= cmd or par_back ~= par then + fail(cmd) + else + pass() + end +end + +----------------------------------------------------------------------------- +-- Tests ASCII line transmission +-- Input +-- len: length of line to be tested +----------------------------------------------------------------------------- +function test_asciiline(len) + local str, str10, back, err + reconnect() + send_command(ECHO_LINE) + str = strrep("x", mod(len, 10)) + str10 = strrep("aZb.c#dAe?", floor(len/10)) + str = str .. str10 + write("testing ", len, " byte(s) line\n") + err = data:send(str, "\n") + if err then fail(err) end + back, err = data:receive() + if err then fail(err) end + if back == str then pass("lines match") + else fail("lines don't match") end +end + +----------------------------------------------------------------------------- +-- Tests closed connection detection +----------------------------------------------------------------------------- +function test_closed() + local str = "This is our little test line" + local len = strlen(str) + local back, err, total + reconnect() + print("testing close while reading line") + send_command(ECHO_BLOCK, len) + data:send(str) + send_command(CLOSE) + -- try to get a line + back, err = data:receive() + if not err then fail("shold have gotten 'closed'.") + elseif err ~= "closed" then fail("got '"..err.."' instead of 'closed'.") + elseif str ~= back then fail("didn't receive what i should 'closed'.") + else pass("rightfull 'closed' received") end + reconnect() + print("testing close while reading block") + send_command(ECHO_BLOCK, len) + data:send(str) + send_command(CLOSE) + -- try to get a line + back, err = data:receive(2*len) + if not err then fail("shold have gotten 'closed'.") + elseif err ~= "closed" then fail("got '"..err.."' instead of 'closed'.") + elseif str ~= back then fail("didn't receive what I should.") + else pass("rightfull 'closed' received") end +end + +----------------------------------------------------------------------------- +-- Tests binary line transmission +-- Input +-- len: length of line to be tested +----------------------------------------------------------------------------- +function test_rawline(len) + local str, str10, back, err + reconnect() + send_command(ECHO_LINE) + str = strrep(strchar(47), mod(len, 10)) + str10 = strrep(strchar(120,21,77,4,5,0,7,36,44,100), floor(len/10)) + str = str .. str10 + write("testing ", len, " byte(s) line\n") + err = data:send(str, "\n") + if err then fail(err) end + back, err = data:receive() + if err then fail(err) end + if back == str then pass("lines match") + else fail("lines don't match") end +end + +----------------------------------------------------------------------------- +-- Tests block transmission +-- Input +-- len: length of block to be tested +----------------------------------------------------------------------------- +function test_block(len) + local half = floor(len/2) + local s1, s2, back, err + reconnect() + send_command(ECHO_BLOCK, len) + write("testing ", len, " byte(s) block\n") + s1 = strrep("x", half) + err = data:send(s1) + if err then fail(err) end + sleep(1) + s2 = strrep("y", len-half) + err = data:send(s2) + if err then fail(err) end + back, err = data:receive(len) + if err then fail(err) end + if back == s1..s2 then pass("blocks match") + else fail("blocks don't match") end +end + +----------------------------------------------------------------------------- +-- Tests if return-timeout was respected +-- delta: time elapsed during transfer +-- t: timeout value +-- err: error code returned by I/O operation +----------------------------------------------------------------------------- +function returntimed_out(delta, t, err) + if err == "timeout" then + if delta + 0.1 >= t then + pass("got right timeout") + return 1 + else + fail("shouldn't have gotten timeout") + end + elseif delta > t then + fail("should have gotten timeout") + end +end + +----------------------------------------------------------------------------- +-- Tests if return-timeout was respected +-- delta: time elapsed during transfer +-- t: timeout value +-- err: error code returned by I/O operation +-- o: operation being executed +----------------------------------------------------------------------------- +function blockedtimed_out(t, s, err, o) + if err == "timeout" then + if s >= t then + pass("got right forced timeout") + return 1 + else + pass("got natural cause timeout (may be wrong)") + return 1 + end + elseif s > t then + if o == "send" then + pass("must have been buffered (may be wrong)") + else + fail("should have gotten timeout") + end + end +end + +----------------------------------------------------------------------------- +-- Tests blocked-timeout conformance +-- Input +-- len: length of block to be tested +-- t: timeout value +-- s: server sleep between transfers +----------------------------------------------------------------------------- +function test_blockedtimeout(len, t, s) + local str, err, back, total + reconnect() + send_command(RECEIVE_BLOCK, len) + send_command(SLEEP, s) + send_command(RECEIVE_BLOCK, len) + write("testing ", len, " bytes, ", t, + "s block timeout, ", s, "s sleep\n") + data:timeout(t) + str = strrep("a", 2*len) + err, total = data:send(str) + if blockedtimed_out(t, s, err, "send") then return end + if err then fail(err) end + send_command(SEND_BLOCK) + send_command(SLEEP, s) + send_command(SEND_BLOCK) + back, err = data:receive(2*len) + if blockedtimed_out(t, s, err, "receive") then return end + if err then fail(err) end + if back == str then pass("blocks match") + else fail("blocks don't match") end +end + +----------------------------------------------------------------------------- +-- Tests return-timeout conformance +-- Input +-- len: length of block to be tested +-- t: timeout value +-- s: server sleep between transfers +----------------------------------------------------------------------------- +function test_returntimeout(len, t, s) + local str, err, back, delta, total + reconnect() + send_command(RECEIVE_BLOCK, len) + send_command(SLEEP, s) + send_command(RECEIVE_BLOCK, len) + write("testing ", len, " bytes, ", t, + "s return timeout, ", s, "s sleep\n") + data:timeout(t, "return") + str = strrep("a", 2*len) + err, total, delta = data:send(str) + print("delta: " .. delta) + if returntimed_out(delta, t, err) then return end + if err then fail(err) end + send_command(SEND_BLOCK) + send_command(SLEEP, s) + send_command(SEND_BLOCK) + back, err, delta = data:receive(2*len) + print("delta: " .. delta) + if returntimed_out(delta, t, err) then return end + if err then fail(err) end + if back == str then pass("blocks match") + else fail("blocks don't match") end +end + +----------------------------------------------------------------------------- +-- Execute all tests +----------------------------------------------------------------------------- +new_test("control connection test") +test_command(EXIT) +test_command(CONNECT) +test_command(CLOSE) +test_command(ECHO_BLOCK, 12234) +test_command(SLEEP, 1111) +test_command(ECHO_LINE) + +new_test("connection close test") +test_closed() + +new_test("binary string test") +test_rawline(1) +test_rawline(17) +test_rawline(200) +test_rawline(3000) +test_rawline(8000) +test_rawline(40000) + +new_test("blocking transfer test") +test_block(1) +test_block(17) +test_block(200) +test_block(3000) +test_block(80000) +test_block(800000) + +new_test("non-blocking transfer test") +-- the value is not important, we only want +-- to test non-blockin I/O anyways +data:timeout(200) +test_block(1) +test_block(17) +test_block(200) +test_block(3000) +test_block(80000) +test_block(800000) +test_block(8000000) + +new_test("character string test") +test_asciiline(1) +test_asciiline(17) +test_asciiline(200) +test_asciiline(3000) +test_asciiline(8000) +test_asciiline(40000) + +new_test("return timeout test") +test_returntimeout(80, .5, 1) +test_returntimeout(80, 1, 0.5) +test_returntimeout(8000, .5, 0) +test_returntimeout(80000, .5, 0) +test_returntimeout(800000, .5, 0) + +new_test("blocked timeout test") +test_blockedtimeout(80, .5, 1) +test_blockedtimeout(80, 1, 1) +test_blockedtimeout(80, 1.5, 1) +test_blockedtimeout(800, 1, 0) +test_blockedtimeout(8000, 1, 0) +test_blockedtimeout(80000, 1, 0) +test_blockedtimeout(800000, 1, 0) + +----------------------------------------------------------------------------- +-- Close connection and exit server. We are done. +----------------------------------------------------------------------------- +new_test("the library has passed all tests") +print("client: closing connection with server") +send_command(CLOSE) +send_command(EXIT) +control:close() +print("client: exiting...") +exit() diff --git a/test/testsrvr.lua b/test/testsrvr.lua new file mode 100644 index 0000000..99ecd2a --- /dev/null +++ b/test/testsrvr.lua @@ -0,0 +1,90 @@ +----------------------------------------------------------------------------- +-- LuaSocket automated test module +-- server.lua +-- This is the server module. It's completely controled by the client module +-- by the use of a control connection. +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Read command definitions +----------------------------------------------------------------------------- +dofile("command.lua") +test_debug_mode() + +----------------------------------------------------------------------------- +-- Bind to address and wait for control connection +----------------------------------------------------------------------------- +server, err = bind(HOST, PORT) +if not server then + print(err) + exit(1) +end +print("server: waiting for control connection...") +control = server:accept() +print("server: control connection stablished!") + +----------------------------------------------------------------------------- +-- Executes a command, detecting any possible failures +-- Input +-- cmd: command to be executed +-- par: command parameters, if needed +----------------------------------------------------------------------------- +function execute_command(cmd, par) + if cmd == CONNECT then + print("server: waiting for data connection...") + data = server:accept() + if not data then + fail("server: unable to start data connection!") + else + print("server: data connection stablished!") + end + elseif cmd == CLOSE then + print("server: closing connection with client...") + if data then + data:close() + data = nil + end + elseif cmd == ECHO_LINE then + str, err = data:receive() + if err then fail("server: " .. err) end + err = data:send(str, "\n") + if err then fail("server: " .. err) end + elseif cmd == ECHO_BLOCK then + str, err = data:receive(par) + if err then fail("server: " .. err) end + err = data:send(str) + if err then fail("server: " .. err) end + elseif cmd == RECEIVE_BLOCK then + str, err = data:receive(par) + elseif cmd == SEND_BLOCK then + err = data:send(str) + elseif cmd == ECHO_TIMEOUT then + str, err = data:receive(par) + if err then fail("server: " .. err) end + err = data:send(str) + if err then fail("server: " .. err) end + elseif cmd == COMMAND then + cmd, par = get_command() + send_command(cmd, par) + elseif cmd == EXIT then + print("server: exiting...") + exit(0) + elseif cmd == SYNC then + print("server: synchronizing...") + send_command(SYNC) + elseif cmd == SLEEP then + print("server: sleeping for " .. par .. " seconds...") + sleep(par) + print("server: woke up!") + end +end + +----------------------------------------------------------------------------- +-- Loop forever, accepting and executing commands +----------------------------------------------------------------------------- +while 1 do + cmd, par = get_command() + if not cmd then fail("server: " .. par) end + print_command(cmd, par) + execute_command(cmd, par) +end