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