if not modules then modules = { } end modules ['mtx-server'] = { version = 1.001, comment = "companion to mtxrun.lua", author = "Hans Hagen & Taco Hoekwater", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } local helpinfo = [[ mtx-server Simple Webserver For Helpers 0.10 start server port to listen to server root scripts sub path index file start on own path ]] local application = logs.application { name = "mtx-server", banner = "Simple Webserver For Helpers 0.10", helpinfo = helpinfo, } local tonumber, tostring, loadfile, type = tonumber, tostring, loadfile, type local find, gsub = string.find, string.gsub local joinpath, filesuffix, dirname, is_qualified_path = file.join, file.suffix, file.dirname, file.is_qualified_path local loaddata = io.loaddata local P, C, patterns, lpegmatch = lpeg.P, lpeg.C, lpeg.patterns, lpeg.match local formatters = string.formatters local urlhashed, urlquery = url.hashed, url.query local report = application.report local gettime = os.gettimeofday or os.clock scripts = scripts or { } scripts.webserver = scripts.webserver or { } local socket = socket or require("socket") ----- http = http or require("socket.http") -- not needed -- The following two lists are taken from webrick (ruby) and -- extended with a few extra suffixes. local mimetypes = { ai = 'application/postscript', asc = 'text/plain', avi = 'video/x-msvideo', bin = 'application/octet-stream', bmp = 'image/bmp', bz2 = 'application/x-bzip2', cer = 'application/pkix-cert', class = 'application/octet-stream', crl = 'application/pkix-crl', crt = 'application/x-x509-ca-cert', css = 'text/css', dms = 'application/octet-stream', doc = 'application/msword', dvi = 'application/x-dvi', eps = 'application/postscript', etx = 'text/x-setext', exe = 'application/octet-stream', gif = 'image/gif', gz = 'application/x-tar', hqx = 'application/mac-binhex40', htm = 'text/html', html = 'text/html', jpe = 'image/jpeg', jpeg = 'image/jpeg', jpg = 'image/jpeg', lha = 'application/octet-stream', lzh = 'application/octet-stream', mov = 'video/quicktime', mpe = 'video/mpeg', mpeg = 'video/mpeg', mpg = 'video/mpeg', pbm = 'image/x-portable-bitmap', pdf = 'application/pdf', pgm = 'image/x-portable-graymap', png = 'image/png', pnm = 'image/x-portable-anymap', ppm = 'image/x-portable-pixmap', ppt = 'application/vnd.ms-powerpoint', ps = 'application/postscript', qt = 'video/quicktime', ras = 'image/x-cmu-raster', rb = 'text/plain', rd = 'text/plain', rgb = 'image/x-rgb', rtf = 'application/rtf', sgm = 'text/sgml', sgml = 'text/sgml', snd = 'audio/basic', tar = 'application/x-tar', tgz = 'application/x-tar', tif = 'image/tiff', tiff = 'image/tiff', txt = 'text/plain', xbm = 'image/x-xbitmap', xls = 'application/vnd.ms-excel', xml = 'text/xml', xpm = 'image/x-xpixmap', xwd = 'image/x-xwindowdump', zip = 'application/zip', } local messages = { [100] = 'Continue', [101] = 'Switching Protocols', [200] = 'OK', [201] = 'Created', [202] = 'Accepted', [203] = 'Non-Authoritative Information', [204] = 'No Content', [205] = 'Reset Content', [206] = 'Partial Content', [300] = 'Multiple Choices', [301] = 'Moved Permanently', [302] = 'Found', [303] = 'See Other', [304] = 'Not Modified', [305] = 'Use Proxy', [307] = 'Temporary Redirect', [400] = 'Bad Request', [401] = 'Unauthorized', [402] = 'Payment Required', [403] = 'Forbidden', [404] = 'Not Found', [405] = 'Method Not Allowed', [406] = 'Not Acceptable', [407] = 'Proxy Authentication Required', [408] = 'Request Timeout', [409] = 'Conflict', [410] = 'Gone', [411] = 'Length Required', [412] = 'Precondition Failed', [413] = 'Request Entity Too Large', [414] = 'Request-URI Too Large', [415] = 'Unsupported Media Type', [416] = 'Request Range Not Satisfiable', [417] = 'Expectation Failed', [500] = 'Internal Server Error', [501] = 'Not Implemented', [502] = 'Bad Gateway', [503] = 'Service Unavailable', [504] = 'Gateway Timeout', [505] = 'HTTP Version Not Supported', } local f_content_length = formatters["Content-Length: %s\r\n"] local f_content_type = formatters["Content-Type: %s\r\n"] local f_error_title = formatters["%s %s

%s %s

"] local handlers = { } local function errormessage(client,configuration,n) local data = f_error_title(n,messages[n],n,messages[n]) report("handling error %s: %s",n,messages[n]) handlers.generic(client,configuration,data,nil,true) end local validpaths, registered = { }, { } function scripts.webserver.registerpath(name) if not registered[name] then local cleanname = gsub(name,"%.%.","deleted-parent") report("registering path: %s",cleanname) validpaths[#validpaths+1] = cleanname registered[name] = true end end function handlers.generic(client,configuration,data,suffix,iscontent) local name = data if not iscontent then report("requested file: %s",name) local fullname = joinpath(configuration.root,name) data = loaddata(fullname) or "" if data == "" then for n=1,#validpaths do local fullname = joinpath(validpaths[n],name) data = loaddata(fullname) or "" if data ~= "" then report("sending generic file: %s",fullname) break end end else report("sending generic file: %s",fullname) end end if data and data ~= "" then client:send("HTTP/1.1 200 OK\r\n") client:send("Connection: close\r\n") client:send(f_content_length(#data)) client:send(f_content_type(suffix and mimetypes[suffix] or "text/html")) client:send("Cache-Control: no-cache, no-store, must-revalidate, max-age=0\r\n") client:send("\r\n") client:send(data) client:send("\r\n") else report("unknown file: %s",tostring(name)) errormessage(client,configuration,404) end end -- return os.date() -- return { content = "crap" } -- return function(configuration,filename) -- return { content = filename } -- end local loaded = { } function handlers.lua(client,configuration,filename,suffix,iscontent,hashed) -- filename will disappear, and become hashed.filename local filename = joinpath(configuration.scripts,filename) if not is_qualified_path(filename) then filename = joinpath(configuration.root,filename) end -- todo: split url in components, see l-url; rather trivial local result, keep = loaded[filename], false if result then report("reusing script: %s",filename) else report("locating script: %s",filename) if lfs.isfile(filename) then report("loading script: %s",filename) result = loadfile(filename) report("return type: %s",type(result)) if result and type(result) == "function" then -- result() should return a table { [type=,] [length=,] content= }, function or string result, keep = result() if keep then report("saving script: %s",type(result)) loaded[filename] = result end end else report("problematic script: %s",filename) errormessage(client,configuration,404) end end if result then if type(result) == "function" then report("running script: %s",filename) result = result(configuration,filename,hashed) -- second argument will become query end if result and type(result) == "string" then result = { content = result } end if result and type(result) == "table" then if result.content then local suffix = result.type or "text/html" local action = handlers[suffix] or handlers.generic action(client,configuration,result.content,suffix,true) -- content elseif result.filename then local suffix = filesuffix(result.filename) or "text/html" local action = handlers[suffix] or handlers.generic action(client,configuration,result.filename,suffix,false) -- filename else report("no content of filename in result") errormessage(client,configuration,404) end else report("no valid result") errormessage(client,configuration,500) end else report("no result") errormessage(client,configuration,404) end end handlers.luc = handlers.lua handlers.html = handlers.htm handlers.pdf = handlers.generic local indices = { "index.htm", "index.html" } local portnumber = 8088 local newline = patterns.newline local spacer = patterns.spacer local whitespace = patterns.whitespace local method = P("GET") + P("POST") local identify = (1-method)^0 * C(method) * spacer^1 * C((1-spacer)^1) * spacer^1 * P("HTTP/") * (1-whitespace)^0 * C(P(1)^0) function scripts.webserver.run(configuration) -- check configuration configuration.port = tonumber(configuration.port or os.getenv("MTX_SERVER_PORT") or portnumber) or portnumber if not configuration.root or not lfs.isdir(configuration.root) then configuration.root = os.getenv("MTX_SERVER_ROOT") or "." end -- locate root and index file in tex tree if not lfs.isdir(configuration.root) then for i=1,#indices do local name = indices[i] local root = resolvers.resolve("path:" .. name) or "" if root ~= "" then configuration.root = root configuration.index = configuration.index or name break end end end configuration.root = dir.expandname(configuration.root) if not configuration.index then for i=1,#indices do local name = indices[i] if lfs.isfile(joinpath(configuration.root,name)) then configuration.index = name -- we will prepend the rootpath later break end end configuration.index = configuration.index or "unknown" end if not configuration.scripts or configuration.scripts == "" then configuration.scripts = dir.expandname(joinpath(configuration.root or ".",configuration.scripts or ".")) end -- so far for checks report("running at port: %s",configuration.port) report("document root: %s",configuration.root or resolvers.ownpath) report("main index file: %s",configuration.index) report("scripts subpath: %s",configuration.scripts) report("context services: http://localhost:%s/mtx-server-ctx-startup.lua",configuration.port) local server = assert(socket.bind("*", configuration.port)) local script = configuration.script while true do -- blocking -- local start = gettime() local client = server:accept() client:settimeout(configuration.timeout or 60) local request, e = client:receive() if e then -- probably a time out -- errormessage(client,configuration,404) else local from = client:getpeername() report("request from: %s",tostring(from)) report("request data: %s",tostring(request)) -- local fullurl = match(request,"(GET) (.+) HTTP/.*$") or "" -- todo: more clever / post -- if fullurl == "" then local method, fullurl, body = lpegmatch(identify,request) if method == "" or fullurl == "" then report("no url") errormessage(client,configuration,404) else -- todo: method: POST fullurl = url.unescapeget(fullurl) report("requested url: %s",fullurl) -- fullurl = socket.url.unescape(fullurl) -- happens later local hashed = urlhashed(fullurl) local query = urlquery(hashed.query) local filename = hashed.path -- hm, not query? hashed.body = body if script then filename = script report("forced script: %s",filename) local suffix = filesuffix(filename) local action = handlers[suffix] or handlers.generic if action then report("performing action: %s",filename) action(client,configuration,filename,suffix,false,hashed) -- filename and no content else report("invalid action: %s",filename) errormessage(client,configuration,404) end elseif filename then local rawname = socket.url.unescape(filename) filename = rawname report("requested action: %s",filename or "?") if find(filename,"%.%.") then filename = nil -- invalid path end if filename == nil or filename == "" or filename == "/" then filename = configuration.index report("invalid filename, forcing: %s",filename) end local suffix = filesuffix(filename) local action = handlers[suffix] or handlers.generic if action then report("performing action: %s",filename or "?") action(client,configuration,filename,suffix,false,hashed) -- filename and no content else report("invalid action: %s",filename or "?") errormessage(client,configuration,404) end else report("invalid request") errormessage(client,configuration,404) end end end client:close() -- report("time spent with client: %0.03f seconds",gettime()-start) end end if environment.argument("auto") then local path = resolvers.findfile("mtx-server.lua") or "." scripts.webserver.run { port = environment.argument("port"), root = environment.argument("root") or dirname(path) or ".", scripts = environment.argument("scripts") or dirname(path) or ".", script = environment.argument("script"), } elseif environment.argument("start") then scripts.webserver.run { port = environment.argument("port"), root = environment.argument("root") or ".", -- "e:/websites/www.pragma-ade.com", index = environment.argument("index"), scripts = environment.argument("scripts"), script = environment.argument("script"), } elseif environment.argument("exporthelp") then application.export(environment.argument("exporthelp"),environment.files[1]) else application.help() end -- mtxrun --script server --start => http://localhost:8088/mtx-server-ctx-startup.lua