xref: /lighttpd1.4/doc/outdated/magnet.txt (revision 25f5085a)
1{{{
2#!rst
3==============
4a power-magnet
5==============
6
7------------------
8Module: mod_magnet
9------------------
10
11
12
13.. contents:: Table of Contents
14
15Requirements
16============
17
18:Version: lighttpd 1.4.12 or higher
19:Packages: lua >= 5.1
20
21Overview
22========
23
24mod_magnet is a module to control the request handling in lighty.
25
26.. note::
27
28  Keep in mind that the magnet is executed in the core of lighty. EVERY long-running operation is blocking
29  ALL connections in the server. You are warned. For time-consuming or blocking scripts use mod_fastcgi and friends.
30
31For performance reasons mod_magnet caches the compiled script. For each script-run the script itself is checked for
32freshness and recompile if necessary.
33
34
35Installation
36============
37
38mod_magnet needs a lighty which is compiled with the lua-support ( --with-lua). Lua 5.1 or higher are required by
39the module. Use "--with-lua=lua5.1" to install on Debian and friends. ::
40
41  server.modules = ( ..., "mod_magnet", ... )
42
43Options
44=======
45
46mod_magnet can attract a request in several stages in the request-handling.
47
48* either at the same level as mod_rewrite, before any parsing of the URL is done
49* or at a later stage, when the doc-root is known and the physical-path is already setup
50
51It depends on the purpose of the script which stage you want to intercept. Usually you want to use
52the 2nd stage where the physical-path which relates to your request is known. At this level you
53can run checks against lighty.env["physical.path"].
54
55::
56
57  magnet.attract-raw-url-to = ( ... )
58  magnet.attract-physical-path-to = ( ... )
59
60You can define multiple scripts when separated by a semicolon. The scripts are executed in the specified
61order. If one of them a returning a status-code, the following scripts will not be executed.
62
63Tables
64======
65
66Most of the interaction between mod_magnet and lighty is done through tables. Tables in lua are hashes (Perl), dictionaries (Java), arrays (PHP), ...
67
68Request-Environment
69-------------------
70
71Lighttpd has its internal variables which are exported as read/write to the magnet.
72
73If "http://example.org/search.php?q=lighty" is requested this results in a request like ::
74
75  GET /search.php?q=lighty HTTP/1.1
76  Host: example.org
77
78When you are using ``attract-raw-url-to`` you can access the following variables:
79
80* parts of the request-line
81
82 * lighty.env["request.uri"] = "/search.php?q=lighty"
83
84* HTTP request-headers
85
86  * lighty.request["Host"] = "example.org"
87
88Later in the request-handling, the URL is split, cleaned up and turned into a physical path name:
89
90* parts of the URI
91
92 * lighty.env["uri.path"] = "/search.php"
93 * lighty.env["uri.path-raw"] = "/search.php"
94 * lighty.env["uri.scheme"] = "http"
95 * lighty.env["uri.authority"] = "example.org"
96 * lighty.env["uri.query"] = "q=lighty"
97
98* filenames, pathnames
99
100 * lighty.env["physical.path"] = "/my-docroot/search.php"
101 * lighty.env["physical.rel-path"] = "/search.php"
102 * lighty.env["physical.doc-root"] = "/my-docroot"
103
104All of them are readable, not all of the are writable (or don't have an effect if you write to them).
105
106As a start, you might want to use those variables for writing: ::
107
108  -- 1. simple rewriting is done via the request.uri
109  lighty.env["request.uri"] = ...
110  return lighty.RESTART_REQUEST
111
112  -- 2. changing the physical-path
113  lighty.env["physical.path"] = ...
114
115  -- 3. changing the query-string
116  lighty.env["uri.query"] = ...
117
118Response Headers
119----------------
120
121If you want to set a response header for your request, you can add a field to the lighty.header[] table: ::
122
123  lighty.header["Content-Type"] = "text/html"
124
125Sending Content
126===============
127
128You can generate your own content and send it out to the clients. ::
129
130  lighty.content = { "<pre>", { filename = "/etc/passwd" }, "</pre>" }
131  lighty.header["Content-Type"] = "text/html"
132
133  return 200
134
135The lighty.content[] table is executed when the script is finished. The elements of the array are processed left to right and the elements can either be a string or a table. Strings are included AS IS into the output of the request.
136
137* Strings
138
139  * are included as is
140
141* Tables
142
143  * filename = "<absolute-path>" is required
144  * offset = <number> [default: 0]
145  * length = <number> [default: size of the file - offset]
146
147Internally lighty will use the sendfile() call to send out the static files at full speed.
148
149Status Codes
150============
151
152You might have seen it already in other examples: In case you are handling the request completely in the magnet you
153can return your own status-codes. Examples are: Redirected, Input Validation, ... ::
154
155  if (lighty.env["uri.scheme"] == "http") then
156    lighty.header["Location"] = "https://" .. lighty.env["uri.authority"] .. lighty.env["request.uri"]
157    return 302
158  end
159
160You every number above and equal to 100 is taken as final status code and finishes the request. No other modules are
161executed after this return.
162
163A special return-code is lighty.RESTART_REQUEST (currently equal to 99) which is usually used in combination with
164changing the request.uri in a rewrite. It restarts the splitting of the request-uri again.
165
166If you return nothing (or nil) the request-handling just continues.
167
168Debugging
169=========
170
171To easy debugging we overloaded the print()-function in lua and redirect the output of print() to the error-log. ::
172
173  print("Host: " .. lighty.request["Host"])
174  print("Request-URI: " .. lighty.env["request.uri"])
175
176
177Examples
178========
179
180Sending text-files as HTML
181--------------------------
182
183This is a bit simplistic, but it illustrates the idea: Take a text-file and cover it in a <pre> tag.
184
185Config-file ::
186
187  magnet.attract-physical-path-to = server.docroot + "/readme.lua"
188
189readme.lua ::
190
191  lighty.content = { "<pre>", { filename = "/README" }, "</pre>" }
192  lighty.header["Content-Type"] = "text/html"
193
194  return 200
195
196Maintenance pages
197------------------
198
199Your side might be on maintenance from time to time. Instead of shutting down the server confusing all
200users, you can just send a maintenance page.
201
202Config-file ::
203
204  magnet.attract-physical-path-to = server.docroot + "/maintenance.lua"
205
206maintenance.lua ::
207
208  require "lfs"
209
210  if (nil == lfs.attributes(lighty.env["physical.doc-root"] .. "/maintenance.html")) then
211    lighty.content = ( lighty.env["physical.doc-root"] .. "/maintenance.html" )
212
213    lighty.header["Content-Type"] = "text/html"
214
215    return 200
216  end
217
218mod_flv_streaming
219-----------------
220
221Config-file ::
222
223  magnet.attract-physical-path-to = server.docroot + "/flv-streaming.lua"
224
225flv-streaming.lua::
226
227  if (lighty.env["uri.query"]) then
228    -- split the query-string
229    get = {}
230    for k, v in string.gmatch(lighty.env["uri.query"], "(%w+)=(%w+)") do
231      get[k] = v
232    end
233
234    if (get["start"]) then
235      -- missing: check if start is numeric and positive
236
237      -- send the FLV header + a seek into the file
238      lighty.content = { "FLV\x1\x1\0\0\0\x9\0\0\0\x9",
239         { filename = lighty.env["physical.path"], offset = get["start"] } }
240      lighty.header["Content-Type"] = "video/x-flv"
241
242      return 200
243    end
244  end
245
246
247selecting a random file from a directory
248----------------------------------------
249
250Say, you want to send a random file (ad-content) from a directory.
251
252To simplify the code and to improve the performance we define:
253
254* all images have the same format (e.g. image/png)
255* all images use increasing numbers starting from 1
256* a special index-file names the highest number
257
258Config ::
259
260  server.modules += ( "mod_magnet" )
261  magnet.attract-physical-path-to = "random.lua"
262
263random.lua ::
264
265  dir = lighty.env["physical.path"]
266
267  f = assert(io.open(dir .. "/index", "r"))
268  maxndx = f:read("*all")
269  f:close()
270
271  ndx = math.random(maxndx)
272
273  lighty.content = { { filename = dir .. "/" .. ndx }}
274  lighty.header["Content-Type"] = "image/png"
275
276  return 200
277
278denying illegal character sequences in the URL
279----------------------------------------------
280
281Instead of implementing mod_security, you might just want to apply filters on the content
282and deny special sequences that look like SQL injection.
283
284A common injection is using UNION to extend a query with another SELECT query.
285
286::
287
288  if (string.find(lighty.env["request.uri"], "UNION%s")) then
289    return 400
290  end
291
292Traffic Quotas
293--------------
294
295If you only allow your virtual hosts a certain amount for traffic each month and want to
296disable them if the traffic is reached, perhaps this helps: ::
297
298  host_blacklist = { ["www.example.org"] = 0 }
299
300  if (host_blacklist[lighty.request["Host"]]) then
301    return 404
302  end
303
304Just add the hosts you want to blacklist into the blacklist table in the shown way.
305
306Complex rewrites
307----------------
308
309If you want to implement caching on your document-root and only want to regenerate
310content if the requested file doesn't exist, you can attract the physical.path: ::
311
312  magnet.attract-physical-path-to = ( server.document-root + "/rewrite.lua" )
313
314rewrite.lua ::
315
316  require "lfs"
317
318  attr = lfs.attributes(lighty.env["physical.path"])
319
320  if (not attr) then
321    -- we couldn't stat() the file for some reason
322    -- let the backend generate it
323
324    lighty.env["uri.path"] = "/dispatch.fcgi"
325    lighty.env["physical.rel-path"] = lighty.env["uri.path"]
326    lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"]
327  fi
328
329luafilesystem
330+++++++++++++
331
332We are requiring the lua-module 'lfs' (http://www.keplerproject.org/luafilesystem/).
333
334I had to compile lfs myself for lua-5.1 which required a minor patch as compat-5.1 is not needed::
335
336  $ wget http://luaforge.net/frs/download.php/1487/luafilesystem-1.2.tar.gz
337  $ wget http://www.lighttpd.net/download/luafilesystem-1.2-lua51.diff
338  $ gzip -cd luafilesystem-1.2.tar.gz | tar xf -
339  $ cd luafilesystem-1.2
340  $ patch -ls -p1 < ../luafilesystem-1.2-lua51.diff
341  $ make install
342
343It will install lfs.so into /usr/lib/lua/5.1/ which is where lua expects the extensions on my system.
344
345SuSE and Gentoo are known to have their own lfs packages and don't require a compile.
346
347Usertracking
348------------
349
350... or how to store data globally in the script-context:
351
352Each script has its own script-context. When the script is started it only contains the lua-functions
353and the special lighty.* name-space. If you want to save data between script runs, you can use the global-script
354context:
355
356::
357
358  if (nil == _G["usertrack"]) then
359    _G["usertrack"] = {}
360  end
361  if (nil == _G["usertrack"][lighty.request["Cookie"]]) then
362    _G["usertrack"][lighty.request["Cookie"]]
363  else
364    _G["usertrack"][lighty.request["Cookie"]] = _G["usertrack"][lighty.request["Cookie"]] + 1
365  end
366
367  print _G["usertrack"][lighty.request["Cookie"]]
368
369The global-context is per script. If you update the script without restarting the server, the context will still be maintained.
370
371Counters
372--------
373
374mod_status support a global statistics page and mod_magnet allows to add and update values in the status page:
375
376Config ::
377
378  status.statistics-url = "/server-counters"
379  magnet.attract-raw-url-to = server.docroot + "/counter.lua"
380
381counter.lua ::
382
383  lighty.status["core.connections"] = lighty.status["core.connections"] + 1
384
385Result::
386
387  core.connections: 7
388  fastcgi.backend.php-foo.0.connected: 0
389  fastcgi.backend.php-foo.0.died: 0
390  fastcgi.backend.php-foo.0.disabled: 0
391  fastcgi.backend.php-foo.0.load: 0
392  fastcgi.backend.php-foo.0.overloaded: 0
393  fastcgi.backend.php-foo.1.connected: 0
394  fastcgi.backend.php-foo.1.died: 0
395  fastcgi.backend.php-foo.1.disabled: 0
396  fastcgi.backend.php-foo.1.load: 0
397  fastcgi.backend.php-foo.1.overloaded: 0
398  fastcgi.backend.php-foo.load: 0
399
400Porting mod_cml scripts
401-----------------------
402
403mod_cml got replaced by mod_magnet.
404
405A CACHE_HIT in mod_cml::
406
407  output_include = { "file1", "file2" }
408
409  return CACHE_HIT
410
411becomes::
412
413  content = { { filename = "/path/to/file1" }, { filename = "/path/to/file2"} }
414
415  return 200
416
417while a CACHE_MISS like (CML) ::
418
419  trigger_handler = "/index.php"
420
421  return CACHE_MISS
422
423becomes (magnet) ::
424
425  lighty.env["request.uri"] = "/index.php"
426
427  return lighty.RESTART_REQUEST
428
429}}}
430