1if not modules then modules = { } end modules ['mtx-server'] = {
2 version = 1.001,
3 comment = "companion to mtxrun.lua",
4 author = "Hans Hagen & Taco Hoekwater",
5 copyright = "PRAGMA ADE / ConTeXt Development Team",
6 license = "see context related readme files"
7}
8
9local helpinfo = [[
10<?xml version="1.0"?>
11<application>
12 <metadata>
13 <entry name="name">mtx-server</entry>
14 <entry name="detail">Simple Webserver For Helpers</entry>
15 <entry name="version">0.10</entry>
16 </metadata>
17 <flags>
18 <category name="basic">
19 <subcategory>
20 <flag name="start"><short>start server</short></flag>
21 <flag name="port"><short>port to listen to</short></flag>
22 <flag name="root"><short>server root</short></flag>
23 <flag name="scripts"><short>scripts sub path</short></flag>
24 <flag name="index"><short>index file</short></flag>
25 <flag name="auto"><short>start on own path</short></flag>
26 </subcategory>
27 </category>
28 </flags>
29</application>
30]]
31
32local application = logs.application {
33 name = "mtx-server",
34 banner = "Simple Webserver For Helpers 0.10",
35 helpinfo = helpinfo,
36}
37
38local tonumber, tostring, loadfile, type = tonumber, tostring, loadfile, type
39local find, gsub = string.find, string.gsub
40local joinpath, filesuffix, dirname, is_qualified_path = file.join, file.suffix, file.dirname, file.is_qualified_path
41local loaddata = io.loaddata
42local P, C, patterns, lpegmatch = lpeg.P, lpeg.C, lpeg.patterns, lpeg.match
43local formatters = string.formatters
44local urlhashed, urlquery = url.hashed, url.query
45local report = application.report
46local gettime = os.gettimeofday or os.clock
47
48scripts = scripts or { }
49scripts.webserver = scripts.webserver or { }
50
51local socket = socket or require("socket")
52
53
54
55
56
57local mimetypes = {
58 ai = 'application/postscript',
59 asc = 'text/plain',
60 avi = 'video/x-msvideo',
61 bin = 'application/octet-stream',
62 bmp = 'image/bmp',
63 bz2 = 'application/x-bzip2',
64 cer = 'application/pkix-cert',
65 class = 'application/octet-stream',
66 crl = 'application/pkix-crl',
67 crt = 'application/x-x509-ca-cert',
68 css = 'text/css',
69 dms = 'application/octet-stream',
70 doc = 'application/msword',
71 dvi = 'application/x-dvi',
72 eps = 'application/postscript',
73 etx = 'text/x-setext',
74 exe = 'application/octet-stream',
75 gif = 'image/gif',
76 gz = 'application/x-tar',
77 hqx = 'application/mac-binhex40',
78 htm = 'text/html',
79 html = 'text/html',
80 jpe = 'image/jpeg',
81 jpeg = 'image/jpeg',
82 jpg = 'image/jpeg',
83 lha = 'application/octet-stream',
84 lzh = 'application/octet-stream',
85 mov = 'video/quicktime',
86 mpe = 'video/mpeg',
87 mpeg = 'video/mpeg',
88 mpg = 'video/mpeg',
89 pbm = 'image/x-portable-bitmap',
90 pdf = 'application/pdf',
91 pgm = 'image/x-portable-graymap',
92 png = 'image/png',
93 pnm = 'image/x-portable-anymap',
94 ppm = 'image/x-portable-pixmap',
95 ppt = 'application/vnd.ms-powerpoint',
96 ps = 'application/postscript',
97 qt = 'video/quicktime',
98 ras = 'image/x-cmu-raster',
99 rb = 'text/plain',
100 rd = 'text/plain',
101 rgb = 'image/x-rgb',
102 rtf = 'application/rtf',
103 sgm = 'text/sgml',
104 sgml = 'text/sgml',
105 snd = 'audio/basic',
106 tar = 'application/x-tar',
107 tgz = 'application/x-tar',
108 tif = 'image/tiff',
109 tiff = 'image/tiff',
110 txt = 'text/plain',
111 xbm = 'image/x-xbitmap',
112 xls = 'application/vnd.ms-excel',
113 xml = 'text/xml',
114 xpm = 'image/x-xpixmap',
115 xwd = 'image/x-xwindowdump',
116 zip = 'application/zip',
117}
118
119local messages = {
120 [100] = 'Continue',
121 [101] = 'Switching Protocols',
122 [200] = 'OK',
123 [201] = 'Created',
124 [202] = 'Accepted',
125 [203] = 'Non-Authoritative Information',
126 [204] = 'No Content',
127 [205] = 'Reset Content',
128 [206] = 'Partial Content',
129 [300] = 'Multiple Choices',
130 [301] = 'Moved Permanently',
131 [302] = 'Found',
132 [303] = 'See Other',
133 [304] = 'Not Modified',
134 [305] = 'Use Proxy',
135 [307] = 'Temporary Redirect',
136 [400] = 'Bad Request',
137 [401] = 'Unauthorized',
138 [402] = 'Payment Required',
139 [403] = 'Forbidden',
140 [404] = 'Not Found',
141 [405] = 'Method Not Allowed',
142 [406] = 'Not Acceptable',
143 [407] = 'Proxy Authentication Required',
144 [408] = 'Request Timeout',
145 [409] = 'Conflict',
146 [410] = 'Gone',
147 [411] = 'Length Required',
148 [412] = 'Precondition Failed',
149 [413] = 'Request Entity Too Large',
150 [414] = 'Request-URI Too Large',
151 [415] = 'Unsupported Media Type',
152 [416] = 'Request Range Not Satisfiable',
153 [417] = 'Expectation Failed',
154 [500] = 'Internal Server Error',
155 [501] = 'Not Implemented',
156 [502] = 'Bad Gateway',
157 [503] = 'Service Unavailable',
158 [504] = 'Gateway Timeout',
159 [505] = 'HTTP Version Not Supported',
160}
161
162local f_content_length = formatters["Content-Length: %s\r\n"]
163local f_content_type = formatters["Content-Type: %s\r\n"]
164local f_error_title = formatters["<head><title>%s %s</title></head><html><h2>%s %s</h2></html>"]
165
166local handlers = { }
167
168local function errormessage(client,configuration,n)
169 local data = f_error_title(n,messages[n],n,messages[n])
170 report("handling error %s: %s",n,messages[n])
171 handlers.generic(client,configuration,data,nil,true)
172end
173
174local validpaths, registered = { }, { }
175
176function scripts.webserver.registerpath(name)
177 if not registered[name] then
178 local cleanname = gsub(name,"%.%.","deleted-parent")
179 report("registering path: %s",cleanname)
180 validpaths[#validpaths+1] = cleanname
181 registered[name] = true
182 end
183end
184
185function handlers.generic(client,configuration,data,suffix,iscontent)
186 local name = data
187 if not iscontent then
188 report("requested file: %s",name)
189 local fullname = joinpath(configuration.root,name)
190 data = loaddata(fullname) or ""
191 if data == "" then
192 for n=1,#validpaths do
193 local fullname = joinpath(validpaths[n],name)
194 data = loaddata(fullname) or ""
195 if data ~= "" then
196 report("sending generic file: %s",fullname)
197 break
198 end
199 end
200 else
201 report("sending generic file: %s",fullname)
202 end
203 end
204 if data and data ~= "" then
205 client:send("HTTP/1.1 200 OK\r\n")
206 client:send("Connection: close\r\n")
207 client:send(f_content_length(#data))
208 client:send(f_content_type(suffix and mimetypes[suffix] or "text/html"))
209 client:send("Cache-Control: no-cache, no-store, must-revalidate, max-age=0\r\n")
210 client:send("\r\n")
211 client:send(data)
212 client:send("\r\n")
213 else
214 report("unknown file: %s",tostring(name))
215 errormessage(client,configuration,404)
216 end
217end
218
219
220
221
222
223
224
225
226
227local loaded = { }
228
229function handlers.lua(client,configuration,filename,suffix,iscontent,hashed)
230 local filename = joinpath(configuration.scripts,filename)
231 if not is_qualified_path(filename) then
232 filename = joinpath(configuration.root,filename)
233 end
234
235 local result, keep = loaded[filename], false
236 if result then
237 report("reusing script: %s",filename)
238 else
239 report("locating script: %s",filename)
240 if lfs.isfile(filename) then
241 report("loading script: %s",filename)
242 result = loadfile(filename)
243 report("return type: %s",type(result))
244 if result and type(result) == "function" then
245
246 result, keep = result()
247 if keep then
248 report("saving script: %s",type(result))
249 loaded[filename] = result
250 end
251 end
252 else
253 report("problematic script: %s",filename)
254 errormessage(client,configuration,404)
255 end
256 end
257 if result then
258 if type(result) == "function" then
259 report("running script: %s",filename)
260 result = result(configuration,filename,hashed)
261 end
262 if result and type(result) == "string" then
263 result = { content = result }
264 end
265 if result and type(result) == "table" then
266 if result.content then
267 local suffix = result.type or "text/html"
268 local action = handlers[suffix] or handlers.generic
269 action(client,configuration,result.content,suffix,true)
270 elseif result.filename then
271 local suffix = filesuffix(result.filename) or "text/html"
272 local action = handlers[suffix] or handlers.generic
273 action(client,configuration,result.filename,suffix,false)
274 else
275 report("no content of filename in result")
276 errormessage(client,configuration,404)
277 end
278 else
279 report("no valid result")
280 errormessage(client,configuration,500)
281 end
282 else
283 report("no result")
284 errormessage(client,configuration,404)
285 end
286end
287
288handlers.luc = handlers.lua
289handlers.html = handlers.htm
290
291local indices = { "index.htm", "index.html" }
292local portnumber = 8088
293
294local newline = patterns.newline
295local spacer = patterns.spacer
296local whitespace = patterns.whitespace
297local method = P("GET")
298 + P("POST")
299local identify = (1-method)^0
300 * C(method)
301 * spacer^1
302 * C((1-spacer)^1)
303 * spacer^1
304 * P("HTTP/")
305 * (1-whitespace)^0
306 * C(P(1)^0)
307
308function scripts.webserver.run(configuration)
309
310 configuration.port = tonumber(configuration.port or os.getenv("MTX_SERVER_PORT") or portnumber) or portnumber
311 if not configuration.root or not lfs.isdir(configuration.root) then
312 configuration.root = os.getenv("MTX_SERVER_ROOT") or "."
313 end
314
315 if not lfs.isdir(configuration.root) then
316 for i=1,#indices do
317 local name = indices[i]
318 local root = resolvers.resolve("path:" .. name) or ""
319 if root ~= "" then
320 configuration.root = root
321 configuration.index = configuration.index or name
322 break
323 end
324 end
325 end
326 configuration.root = dir.expandname(configuration.root)
327 if not configuration.index then
328 for i=1,#indices do
329 local name = indices[i]
330 if lfs.isfile(joinpath(configuration.root,name)) then
331 configuration.index = name
332 break
333 end
334 end
335 configuration.index = configuration.index or "unknown"
336 end
337 if not configuration.scripts or configuration.scripts == "" then
338 configuration.scripts = dir.expandname(joinpath(configuration.root or ".",configuration.scripts or "."))
339 end
340
341 report("running at port: %s",configuration.port)
342 report("document root: %s",configuration.root or resolvers.ownpath)
343 report("main index file: %s",configuration.index)
344 report("scripts subpath: %s",configuration.scripts)
345 report("context services: http://localhost:%s/mtx-server-ctx-startup.lua",configuration.port)
346 local server = assert(socket.bind("*", configuration.port))
347 local script = configuration.script
348 while true do
349
350 local client = server:accept()
351 client:settimeout(configuration.timeout or 60)
352 local request, e = client:receive()
353 if e then
354
355
356 else
357 local from = client:getpeername()
358 report("request from: %s",tostring(from))
359 report("request data: %s",tostring(request))
360
361
362 local method, fullurl, body = lpegmatch(identify,request)
363 if method == "" or fullurl == "" then
364 report("no url")
365 errormessage(client,configuration,404)
366 else
367
368
369
370 fullurl = url.unescapeget(fullurl)
371 report("requested url: %s",fullurl)
372
373 local hashed = urlhashed(fullurl)
374 local query = urlquery(hashed.query)
375 local filename = hashed.path
376 hashed.body = body
377 if script then
378 filename = script
379 report("forced script: %s",filename)
380 local suffix = filesuffix(filename)
381 local action = handlers[suffix] or handlers.generic
382 if action then
383 report("performing action: %s",filename)
384 action(client,configuration,filename,suffix,false,hashed)
385 else
386 report("invalid action: %s",filename)
387 errormessage(client,configuration,404)
388 end
389 elseif filename then
390 local rawname = socket.url.unescape(filename)
391 filename = rawname
392 report("requested action: %s",filename or "?")
393 if find(filename,"%.%.") then
394 filename = nil
395 end
396 if filename == nil or filename == "" or filename == "/" then
397 filename = configuration.index
398 report("invalid filename, forcing: %s",filename)
399 end
400 local suffix = filesuffix(filename)
401 local action = handlers[suffix] or handlers.generic
402 if action then
403 report("performing action: %s",filename or "?")
404 action(client,configuration,filename,suffix,false,hashed)
405 else
406 report("invalid action: %s",filename or "?")
407 errormessage(client,configuration,404)
408 end
409 else
410 report("invalid request")
411 errormessage(client,configuration,404)
412 end
413 end
414 end
415 client:close()
416
417 end
418end
419
420if environment.argument("auto") then
421 local path = resolvers.findfile("mtx-server.lua") or "."
422 scripts.webserver.run {
423 port = environment.argument("port"),
424 root = environment.argument("root") or dirname(path) or ".",
425 scripts = environment.argument("scripts") or dirname(path) or ".",
426 script = environment.argument("script"),
427 }
428elseif environment.argument("start") then
429 scripts.webserver.run {
430 port = environment.argument("port"),
431 root = environment.argument("root") or ".",
432 index = environment.argument("index"),
433 scripts = environment.argument("scripts"),
434 script = environment.argument("script"),
435 }
436elseif environment.argument("exporthelp") then
437 application.export(environment.argument("exporthelp"),environment.files[1])
438else
439 application.help()
440end
441
442
443 |