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
290handlers.pdf = handlers.generic
291
292local indices = { "index.htm", "index.html" }
293local portnumber = 8088
294
295local newline = patterns.newline
296local spacer = patterns.spacer
297local whitespace = patterns.whitespace
298local method = P("GET")
299 + P("POST")
300local identify = (1-method)^0
301 * C(method)
302 * spacer^1
303 * C((1-spacer)^1)
304 * spacer^1
305 * P("HTTP/")
306 * (1-whitespace)^0
307 * C(P(1)^0)
308
309function scripts.webserver.run(configuration)
310
311 configuration.port = tonumber(configuration.port or os.getenv("MTX_SERVER_PORT") or portnumber) or portnumber
312 if not configuration.root or not lfs.isdir(configuration.root) then
313 configuration.root = os.getenv("MTX_SERVER_ROOT") or "."
314 end
315
316 if not lfs.isdir(configuration.root) then
317 for i=1,#indices do
318 local name = indices[i]
319 local root = resolvers.resolve("path:" .. name) or ""
320 if root ~= "" then
321 configuration.root = root
322 configuration.index = configuration.index or name
323 break
324 end
325 end
326 end
327 configuration.root = dir.expandname(configuration.root)
328 if not configuration.index then
329 for i=1,#indices do
330 local name = indices[i]
331 if lfs.isfile(joinpath(configuration.root,name)) then
332 configuration.index = name
333 break
334 end
335 end
336 configuration.index = configuration.index or "unknown"
337 end
338 if not configuration.scripts or configuration.scripts == "" then
339 configuration.scripts = dir.expandname(joinpath(configuration.root or ".",configuration.scripts or "."))
340 end
341
342 report("running at port: %s",configuration.port)
343 report("document root: %s",configuration.root or resolvers.ownpath)
344 report("main index file: %s",configuration.index)
345 report("scripts subpath: %s",configuration.scripts)
346 report("context services: http://localhost:%s/mtx-server-ctx-startup.lua",configuration.port)
347 local server = assert(socket.bind("*", configuration.port))
348 local script = configuration.script
349 while true do
350
351 local client = server:accept()
352 client:settimeout(configuration.timeout or 60)
353 local request, e = client:receive()
354 if e then
355
356
357 else
358 local from = client:getpeername()
359 report("request from: %s",tostring(from))
360 report("request data: %s",tostring(request))
361
362
363 local method, fullurl, body = lpegmatch(identify,request)
364 if method == "" or fullurl == "" then
365 report("no url")
366 errormessage(client,configuration,404)
367 else
368
369
370
371 fullurl = url.unescapeget(fullurl)
372 report("requested url: %s",fullurl)
373
374 local hashed = urlhashed(fullurl)
375 local query = urlquery(hashed.query)
376 local filename = hashed.path
377 hashed.body = body
378 if script then
379 filename = script
380 report("forced script: %s",filename)
381 local suffix = filesuffix(filename)
382 local action = handlers[suffix] or handlers.generic
383 if action then
384 report("performing action: %s",filename)
385 action(client,configuration,filename,suffix,false,hashed)
386 else
387 report("invalid action: %s",filename)
388 errormessage(client,configuration,404)
389 end
390 elseif filename then
391 local rawname = socket.url.unescape(filename)
392 filename = rawname
393 report("requested action: %s",filename or "?")
394 if find(filename,"%.%.") then
395 filename = nil
396 end
397 if filename == nil or filename == "" or filename == "/" then
398 filename = configuration.index
399 report("invalid filename, forcing: %s",filename)
400 end
401 local suffix = filesuffix(filename)
402 local action = handlers[suffix] or handlers.generic
403 if action then
404 report("performing action: %s",filename or "?")
405 action(client,configuration,filename,suffix,false,hashed)
406 else
407 report("invalid action: %s",filename or "?")
408 errormessage(client,configuration,404)
409 end
410 else
411 report("invalid request")
412 errormessage(client,configuration,404)
413 end
414 end
415 end
416 client:close()
417
418 end
419end
420
421if environment.argument("auto") then
422 local path = resolvers.findfile("mtx-server.lua") or "."
423 scripts.webserver.run {
424 port = environment.argument("port"),
425 root = environment.argument("root") or dirname(path) or ".",
426 scripts = environment.argument("scripts") or dirname(path) or ".",
427 script = environment.argument("script"),
428 }
429elseif environment.argument("start") then
430 scripts.webserver.run {
431 port = environment.argument("port"),
432 root = environment.argument("root") or ".",
433 index = environment.argument("index"),
434 scripts = environment.argument("scripts"),
435 script = environment.argument("script"),
436 }
437elseif environment.argument("exporthelp") then
438 application.export(environment.argument("exporthelp"),environment.files[1])
439else
440 application.help()
441end
442
443
444 |