Created
January 26, 2012 15:23
-
-
Save minikomi/1683259 to your computer and use it in GitHub Desktop.
lua server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- -*-mode: C++; style: K&R; c-basic-offset: 4 ; -*- */ | |
-- | |
-- A simple HTTP server written in Lua, using the socket primitives | |
-- in 'libhttpd.so'. | |
-- | |
-- This server will handle multiple virtual hosts. The only requirement | |
-- is that each virtual host must have the documents located beneath | |
-- a common tree. Note that directory indexes are not (yet) supported. | |
-- | |
-- For example to serve the three virtual hosts: | |
-- lappy | |
-- bob | |
-- localhost | |
-- | |
-- You should have a directory layout such as: | |
-- | |
-- ./vhosts/ | |
-- | |
-- ./vhosts/bob/ | |
-- ./vhosts/bob/htdocs/ | |
-- ./vhosts/bob/htdocs/index.html [etc] | |
-- ./vhosts/bob/logs/ | |
-- | |
-- ./vhosts/lappy/ | |
-- ./vhosts/lappy/htdocs/ | |
-- ./vhosts/lappy/htdocs/index.html [etc] | |
-- ./vhosts/lappy/logs/ | |
-- | |
-- ./vhosts/localhost/ | |
-- ./vhosts/localhost/htdocs/ | |
-- ./vhosts/localhost/htdocs/index.html [etc] | |
-- ./vhosts/localhost/logs/ | |
-- | |
-- | |
-- (If you wish to allow for fully qualified hosts simply add symbolic | |
-- links to suit your hostnames.) | |
-- | |
-- | |
-- If a request arrives for a virtual host which you are not serving | |
-- then it will be passed to the faux server "default". Simply create | |
-- a symbolic link to the server you wish to be your default host: | |
-- | |
-- cd ./vhosts | |
-- ln -s lappy default | |
-- | |
-- | |
-- | |
-- The code is NOT secure | |
-- ---------------------- | |
-- | |
-- By that I mean that I won't promise any security. I take the obvious | |
-- step of filtering '/../' from incoming requests and Hostnames, but there | |
-- may be other weaknesses. | |
-- | |
-- Error messages are escaped by default, to prevent XSS attacks. | |
-- | |
-- DOS attacks might be possible, although again the obvious cases | |
-- are covered. | |
-- | |
-- | |
-- Steve Kemp | |
-- -- | |
-- http://www.steve.org.uk/ | |
-- | |
-- $Id: httpd.lua,v 1.31 2005/10/31 17:08:22 steve Exp $ | |
-- | |
-- load the socket library | |
-- | |
socket = require( "libhttpd" ); | |
print( "\n\nLoaded the socket library, version: \n " .. socket.version ); | |
-- | |
-- A table of MIME types. | |
-- These values will be loaded from /etc/mime.types if available, otherwise | |
-- a minimum set of defaults will be used. | |
-- | |
mime = {}; | |
-- | |
-- Start a server upon the given port, using the given | |
-- root directory path. | |
-- | |
-- The root path is designed to contain sub-directories for | |
-- each given virtual host. | |
-- | |
function start_server( port, root ) | |
running = 1; | |
-- | |
-- Bind a socket to the given port | |
-- | |
local listener = socket.bind( port ); | |
if ( listener == nil ) then | |
error("Error in bind()"); | |
end | |
-- | |
-- Print some status messages. | |
-- | |
print( "\nListening upon:" ); | |
print( " http://localhost:" .. port .. "/" ); | |
print( "\nLoading virtual hosts from beneath: " ); | |
print( " " .. root .. "\n\n"); | |
-- | |
-- Loop accepting requests. | |
-- | |
while ( running == 1 ) do | |
processConnection( root, listener ); | |
end | |
-- | |
-- Finished. | |
-- | |
socket.close( listener ); | |
end | |
-- | |
-- Process a single incoming connection. | |
-- | |
function processConnection( root, listener ) | |
-- | |
-- Accept a new connection | |
-- | |
client,ip = socket.accept( listener ); | |
found = 0; -- Found the end of the HTTP headers? | |
chunk = 0; -- Count of data read from client socket | |
size = 0; -- Total size of incoming request. | |
code = 0; -- Status code we send to the client | |
request = ""; -- Request body read from client. | |
-- | |
-- Read in a response from the client, terminating at the first | |
-- '\r\n\r\n' line - this is the end of the HTTP header request. | |
-- | |
-- Also break out of this loop if we read ten packets of data | |
-- from the client but didn't manage to find a HTTP header end. | |
-- This should help protect us from DOSes. | |
-- | |
while ( ( found == 0 ) and ( chunk < 10 ) ) do | |
length, data = socket.read(client); | |
if ( length < 1 ) then | |
found = 1; | |
end | |
size = size + length; | |
request = request .. data; | |
position,len = string.find( request, '\r\n\r\n' ); | |
if ( position ~= nil ) then | |
found = 1; | |
end | |
chunk = chunk + 1; | |
end | |
-- | |
-- OK We now have a complete HTTP request of 'size' bytes long | |
-- stored in 'request'. | |
-- | |
-- | |
-- Find the requested path. | |
-- | |
_, _, method, path, major, minor = string.find(request, "([A-Z]+) (.+) HTTP/(%d).(%d)"); | |
-- | |
-- We only handle GET requests. | |
-- | |
if ( method ~= "GET" ) then | |
error = "Method not implemented"; | |
if ( method == nil ) then | |
error = error .. "."; | |
else | |
error = error .. ": " .. urlEncode( method ); | |
end | |
size = sendError( client, 501, error ); | |
socket.close( client ); | |
return size, "501"; | |
end | |
-- | |
-- Decode the requested path. | |
-- | |
path = urlDecode( path ); | |
-- | |
-- find the Virtual Host which we need for serving, and find the | |
-- user agent and referer for logging purposes. | |
-- | |
_, _, host = string.find(request, "Host: ([^:\r\n]+)"); | |
_, _, agent = string.find(request, "Agent: ([^\r\n]+)"); | |
_, _, referer = string.find(request, "Referer: ([^\r\n]+)"); | |
-- | |
-- If there was no "Host:" header then use "default". | |
-- | |
if ( host == nil ) then | |
host = "default"; | |
end | |
-- | |
-- Is this a host that we're dealing with? | |
-- | |
-- For this code to work we would ideally use the "os.stat" patch | |
-- against Lua 5.0 which can be found at: | |
-- | |
-- http://lua-users.org/wiki/PeterShook | |
-- | |
-- Instead we use our own "is_dir" function inside the socket | |
-- library. | |
-- | |
info = socket.is_dir( root .. host ); | |
if ( not info ) then | |
host = 'default'; | |
end | |
-- | |
-- If the request was for '/finish' then terminate ourselves | |
-- | |
if ( path == "/finish" ) then | |
running = 0; | |
socket.write( client, "HTTP/1.0 200 OK\r\n" ); | |
socket.write( client, "Server: lua-httpd " .. socket.version .. "\r\n" ); | |
socket.write( client, "Content-type: text/html\r\n" ); | |
socket.write( client, "Connection: close\r\n\r\n" ); | |
socket.write( client, "<html><head><title>Server Terminated</title></head>" ); | |
socket.write( client, "<body><h1>Server Terminated</h1><p>As per your request.</p></body></html>"); | |
else | |
-- | |
-- Otherwise attempt to handle the connection. | |
-- | |
size, code = handleRequest( root, host, path, client ); | |
end | |
if ( agent == nil ) then agent = "-" end; | |
if ( referer == nil ) then referer = "-" end; | |
if ( code == nill ) then code = 0 ; end; | |
-- | |
-- Log the access in something resembling the Apache common | |
-- log format. | |
-- | |
-- Note: The logging does not write the name of the virtual host. | |
-- | |
logAccess( root, host, ip, path, code, size, agent, referer, major, minor ); | |
if ( running == 0 ) then | |
print( "Terminating as per request from " .. ip ); | |
end | |
-- | |
-- Close the client connection. | |
-- | |
socket.close( client ); | |
end | |
-- | |
-- Attempt to serve the given path to the given client | |
-- | |
function handleDirectory( client, path, request ) | |
a = socket.readdir( path ); | |
socket.write( client, "HTTP/1.0 200 OK\r\n" ); | |
socket.write( client, "Server: lua-httpd " .. socket.version .. "\r\n" ); | |
socket.write( client, "Content-type: text/html\r\n" ); | |
socket.write( client, "Connection: close\r\n\r\n" ); | |
msg = ""; | |
msg = msg .. "<html><head><title>Files in " .. request .. "</title></head>"; | |
msg = msg .. "<body><h2>Files in " .. request .. "</h2>\n"; | |
msg = msg .. "<ul>\n"; | |
for i=0,table.getn(a) do | |
item = a[i]; | |
if ( socket.is_dir( path .. "/" .. item ) ) then | |
item = item .. "/"; | |
end | |
msg = msg .. "<li><a href=\"" .. item .. "\">" .. item .. "</a></li>\n"; | |
end | |
msg = msg .. "</ul></body></html>\n"; | |
socket.write( client, msg ); | |
return string.len(msg) ; | |
end | |
-- | |
-- Attempt to serve the given path to the given client | |
-- | |
function handleRequest( root, host, path, client ) | |
-- | |
-- Local file | |
-- | |
file = path; | |
-- | |
-- File must be beneath the vhost root. | |
-- | |
file = root .. host .. "/htdocs" .. file ; | |
-- | |
-- Attempt to sanitize the input Virtual Host + requested path. | |
-- | |
file = string.strip( file ); | |
-- | |
-- Add a trailing "index.html" to paths ending in / if such | |
-- a file exists. | |
-- | |
-- Otherwise if it is a directory then serve it. | |
-- | |
if ( string.endsWith( file, "/" ) ) then | |
tmp = file .. "index.html"; | |
if ( fileExists( tmp ) ) then | |
file = tmp; | |
else | |
if ( socket.is_dir( file ) ) then | |
size = handleDirectory( client, file, path ) ; | |
socket.close( client ); | |
return size, "200"; | |
end | |
end | |
end | |
-- | |
-- Open the file and return an error if it fails. | |
-- | |
if ( fileExists( file ) == false ) then | |
size = sendError( client, 404,"File not found " .. urlEncode( path ) ); | |
socket.close( client ); | |
return size, "404"; | |
end; | |
-- | |
-- Find the suffix to get the mime.type. | |
-- | |
_, _, ext = string.find( file, "\.([^\.]+)$" ); | |
if ( ext == nil ) then | |
ext = "html"; -- HACK | |
end | |
type = mime[ext]; | |
if ( type == nil ) then type = 'text/plain' ; end; | |
-- | |
-- Send out the header. | |
-- | |
socket.write( client, "HTTP/1.0 200 OK\r\n" ); | |
socket.write( client, "Server: lua-httpd " .. socket.version .. "\r\n" ); | |
socket.write( client, "Content-type: " .. type .. "\r\n" ); | |
socket.write( client, "Connection: close\r\n\r\n" ); | |
-- | |
-- Read the file, and then serve it. | |
-- | |
f = io.open( file, "rb" ); | |
size = fileSize( f ); | |
local t = f:read("*all") | |
socket.write( client, t ); | |
f:close(); | |
return size, "200" ; | |
end | |
-- | |
-- Send the given error message to the client. | |
-- Return the length of data sent to the client so we know what to log. | |
-- | |
function sendError( client, status, str ) | |
message = "HTTP/1.0 " .. status .. " OK\r\n" ; | |
message = message .. "Server: lua-httpd " .. socket.version .. "\r\n"; | |
message = message .. "Content-type: text/html\r\n"; | |
message = message .. "Connection: close\r\n\r\n" ; | |
message = message .. "<html><head><title>Error</title></head>" ; | |
message = message .. "<body><h1>Error</h1" ; | |
message = message .. "<p>" .. str .. "</p></body></html>" ; | |
socket.write( client, message ); | |
return string.len(message) ; | |
end | |
-- | |
-- Utility function: Determine the size of an open file. | |
-- | |
function fileSize (file) | |
local current = file:seek() -- get current position | |
local size = file:seek("end") -- get file size | |
file:seek("set", current) -- restore position | |
return size | |
end | |
-- | |
-- Utility function: Determine whether the given file exists. | |
-- | |
function fileExists (file) | |
local f = io.open(file, "rb"); | |
if f == nil then | |
return false; | |
else | |
f:close(); | |
return true; | |
end | |
end | |
-- | |
-- Read the mime file and setup mime types | |
-- | |
function loadMimeFile(file, table) | |
local f = io.open( file, "r" ); | |
while true do | |
local line = f:read() | |
if line == nil then break end | |
_, _, type, name = string.find( line, "^(.*)\t+([^\t]+)$" ); | |
if ( type ~= nil ) then | |
for w in string.gfind(name, "([^%s]+)") do | |
table[w] = type; | |
end | |
end | |
end | |
end | |
-- | |
-- Utility function: Does the string end with the given suffix? | |
-- | |
function string.endsWith(String,End) | |
return End=='' or string.sub(String,-string.len(End))==End | |
end | |
-- | |
-- Strip path traversal requests. | |
-- | |
function string.strip( str ) | |
return( string.gsub( str, "/../", "" ) ); | |
end | |
-- | |
-- Utility function: URL encoding function | |
-- | |
function urlEncode(str) | |
if (str) then | |
str = string.gsub (str, "\n", "\r\n") | |
str = string.gsub (str, "([^%w ])", | |
function (c) return string.format ("%%%02X", string.byte(c)) end) | |
str = string.gsub (str, " ", "+") | |
end | |
return str | |
end | |
-- | |
-- Utility function: URL decode function | |
-- | |
function urlDecode(str) | |
str = string.gsub (str, "+", " ") | |
str = string.gsub (str, "%%(%x%x)", function(h) return string.char(tonumber(h,16)) end) | |
str = string.gsub (str, "\r\n", "\n") | |
return str | |
end | |
-- | |
-- Log an access request. | |
-- | |
function logAccess( root, host, ip, request, status, size, agent, referer, major, minor ) | |
date = os.date("%m/%b/%Y:%H:%M:%S +0000"); | |
-- | |
-- Format the logging line. | |
-- | |
log = string.format( '%s - - [%s] "GET %s HTTP/%s.%s" %s %s "%s" "%s"', ip, date, request, major, minor, status, size, referer, agent ); | |
logfile = root .. host .. "/logs/access.log" | |
-- | |
-- Open logfile for appending. | |
-- | |
file = io.open( logfile , "a" ); | |
if ( file ~= nil ) then | |
file:write( log .. "\n" ); | |
file:close(); | |
else | |
print( "WARNING : Unable to open logfile for writing : " .. logfile ); | |
end | |
print( log ); | |
end | |
-- | |
----- | |
--- | |
-- | |
-- This is the start of the real code. Now that our functions have been | |
-- defined we actually execute from this point onwards. | |
-- | |
--- | |
---- | |
-- | |
-- | |
-- Setup the MIME types our server will use for serving files. | |
-- | |
if ( fileExists( "/etc/mime.types" ) ) then | |
loadMimeFile( "/etc/mime.types", mime ); | |
else | |
-- | |
-- The global MIME types file does not exist. | |
-- Setup minimal defaults. | |
-- | |
print( "WARNING: /etc/mime.types could not be read." ); | |
print( " Running with minimal MIME types." ); | |
mime[ "html" ] = "text/html"; | |
mime[ "txt" ] = "text/plain"; | |
mime[ "jpg" ] = "image/jpeg"; | |
mime[ "jpeg" ] = "image/jpeg"; | |
mime[ "gif" ] = "image/gif"; | |
mime[ "png" ] = "image/png"; | |
end | |
-- | |
-- Now start the server. | |
-- | |
-- | |
start_server( 4444, "./vhosts/" ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment