Project

General

Profile

Mod magnet » History » Revision 31

Revision 30 (icy, 2007-03-16 18:02) → Revision 31/119 (icy, 2007-03-16 20:07)

{{{ 
 #!rst 
 ============== 
 a power-magnet 
 ============== 

 ------------------ 
 Module: mod_magnet 
 ------------------ 



 .. contents:: Table of Contents 

 Requirements 
 ============ 

 :Version: lighttpd 1.4.12 or higher 
 :Packages: lua >= 5.1 

 Overview 
 ======== 

 mod_magnet is a module to control the request handling in lighty.  

 .. note:: 

   Keep in mind that the magnet is executed in the core of lighty. EVERY long-running operation is blocking  
   ALL connections in the server. You are warned. For time-consuming or blocking scripts use mod_fastcgi and friends. 

 For performance reasons mod_magnet caches the compiled script. For each script-run the script itself is checked for  
 freshness and recompile if neccesary. 


 Installation 
 ============ 

 mod_magnet needs a lighty which is compiled with the lua-support ( --with-lua). Lua 5.1 or higher are required by 
 the module. Use "--with-lua=lua5.1" to install on Debian and friends. :: 

   server.modules = ( ..., "mod_magnet", ... ) 

 luafilesystem 
 ------------- 

 Sometimes you have to use a Lua module like LuaFileSystem (http://www.keplerproject.org/luafilesystem/).  

 I had to compile lfs myself for lua-5.1 which required a minor patch as compat-5.1 is not needed:: 

   $ wget http://luaforge.net/frs/download.php/1487/luafilesystem-1.2.tar.gz 
   $ wget http://www.lighttpd.net/download/luafilesystem-1.2-lua51.diff 
   $ gzip -cd luafilesystem-1.2.tar.gz | tar xf - 
   $ cd luafilesystem-1.2 
   $ patch -ls -p1 < ../luafilesystem-1.2-lua51.diff 
   $ make install 

 It will install lfs.so into /usr/lib/lua/5.1/ which is where lua expects the extensions on my system. 

 SuSE is known to have its own lfs packages and don't require a compile. 
 An ebuild for LuaFileSystem for Gentoo Linux can be found in https://bugs.gentoo.org/show_bug.cgi?id=147440 

 Options 
 ======= 

 mod_magnet can attract a request in several stages in the request-handling.  

 * either at the same level as mod_rewrite, before any parsing of the URL is done 
 * or at a later stage, when the doc-root is known and the physical-path is already setup 

 It depends on the purpose of the script which stage you want to intercept. Usually you want to use 
 the 2nd stage where the physical-path which relates to your request is known. At this level you 
 can run checks against lighty.env["physical.path"]. 

 :: 

   magnet.attract-raw-url-to = ( ... ) 
   magnet.attract-physical-path-to = ( ... ) 

 You can define multiple scripts when separated by a semicolon. The scripts are executed in the specified  
 order. If one of them a returning a status-code, the following scripts will not be executed. 

 Tables 
 ====== 

 Most of the interaction between between mod_magnet and lighty is done through tables. Tables in lua are hashes (Perl, Ruby), dictionaries (Java, Python), associative arrays (PHP), ... 

 Request-Environment 
 ------------------- 

 Lighttpd has its internal variables which are exported as read/write to the magnet.  

 If "http://example.org/search.php?q=lighty" is requested this results in a request like :: 

   GET /search.php?q=lighty HTTP/1.1 
   Host: example.org 

 When you are using ``attract-raw-url-to`` you can access the following variables: 

 * parts of the request-line 

  * lighty.env["request.uri"] = "/search.php?q=lighty" 

 * HTTP request-headers 

   * lighty.request["Host"] = "example.org" 

 Later in the request-handling, the URL is splitted, cleaned up and turned into a physical path name: 

 * parts of the URI 

  * lighty.env["uri.path"] = "/search.php" 
  * lighty.env["uri.path-raw"] = "/search.php" 
  * lighty.env["uri.scheme"] = "http" 
  * lighty.env["uri.authority"] = "example.org" 
  * lighty.env["uri.query"] = "q=lighty" 

 * filenames, pathnames 

  * lighty.env["physical.path"] = "/my-docroot/search.php" 
  * lighty.env["physical.rel-path"] = "/search.php" 
  * lighty.env["physical.doc-root"] = "/my-docroot" 

 All of them are readable, not all of the are writable (or don't have an effect if you write to them).  

 As a start, you might want to use those variables for writing: :: 

   -- 1. simple rewriting is done via the request.uri 
   lighty.env["request.uri"] = ...  
   return lighty.RESTART_REQUEST 

   -- 2. changing the physical-path 
   lighty.env["physical.path"] = ... 

   -- 3. changing the query-string 
   lighty.env["uri.query"] = ... 

 Response Headers 
 ---------------- 

 If you want to set a response header for your request, you can add a field to the lighty.header[] table: :: 

   lighty.header["Content-Type"] = "text/html" 

 Sending Content 
 =============== 

 You can generate your own content and send it out to the clients. :: 

   lighty.content = { "<pre>", { filename = "/etc/passwd" }, "</pre>" } 
   lighty.header["Content-Type"] = "text/html" 

   return 200 

 The 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. 

 * Strings 

   * are included as is 

 * Tables 

   * filename = "<absolute-path>" is required 
   * offset = <number> [default: 0] 
   * length = <number> [default: size of the file - offset] 

 Internally lighty will use the sendfile() call to send out the static files at full speed. 

 Status Codes 
 ============ 

 You might have seen it already in other examples: In case you are handling the request completly in the magnet you 
 can return your own status-codes. Examples are: Redirected, Input Validation, ... :: 

   if (lighty.env["uri.scheme"] == "http") then 
     lighty.header["Location"] = "https://" .. lighty.env["uri.authority"] .. lighty.env["request.uri"] 
     return 302 
   end 

 You every number above and equal to 100 is taken as final status code and finishes the request. No other modules are  
 executed after this return. 

 A special return-code is lighty.RESTART_REQUEST (currently equal to 99) which is usually used in combination with  
 changing the request.uri in a rewrite. It restarts the splitting of the request-uri again. 

 If you return nothing (or nil) the request-handling just continues. 

 Debugging 
 ========= 

 To easy debugging we overloaded the print()-function in lua and redirect the output of print() to the error-log. :: 

   print("Host: " .. lighty.request["Host"]) 
   print("Request-URI: " .. lighty.env["request.uri"]) 


 Examples 
 ======== 

 Sending text-files as HTML 
 -------------------------- 

 This is a bit simplistic, but it illustrates the idea: Take a text-file and cover it in a <pre> tag. 

 Config-file :: 

   magnet.attract-physical-path-to = (server.docroot + "/readme.lua") 

 readme.lua :: 

   lighty.content = { "<pre>", { filename = "/README" }, "</pre>" } 
   lighty.header["Content-Type"] = "text/html" 
  
   return 200 

 Maintainance pages 
 ------------------ 

 Your site might be on maintainance from time to time. Instead of shutting down the server confusing all 
 users, you can just send a maintainance page. 

 Config-file :: 

   magnet.attract-physical-path-to = (server.docroot + "/maintainance.lua") 

 maintainance.lua :: 

   if not (nil == lighty.stat(lighty.env["physical.doc-root"] .. "/maintainance.html")) then 
     lighty.content = { { filename = lighty.env["physical.doc-root"] .. "/maintainance.html" } } 

     lighty.header["Content-Type"] = "text/html" 

     return 200 
   end 

 mod_flv_streaming 
 ----------------- 

 Config-file :: 

   magnet.attract-physical-path-to = (server.docroot + "/flv-streaming.lua") 

 flv-streaming.lua:: 

   if (lighty.env["uri.query"]) then 
     -- split the query-string 
     get = {} 
     for k, v in string.gmatch(lighty.env["uri.query"], "(%w+)=(%w+)") do 
       get[k] = v 
     end 

     if (get["start"]) then 
       -- missing: check if start is numeric and positive 

       -- send te FLV header + a seek into the file 
       lighty.content = { "FLV\x1\x1\0\0\0\x9\0\0\0\x9",  
          { filename = lighty.env["physical.path"], offset = get["start"] } } 
       lighty.header["Content-Type"] = "video/x-flv" 

       return 200 
     end 
   end 

  
 selecting a random file from a directory 
 ---------------------------------------- 

 Say, you want to send a random file (ad-content) from a directory.  

 To simplify the code and to improve the performance we define: 

 * all images have the same format (e.g. image/png) 
 * all images use increasing numbers starting from 1 
 * a special index-file names the highest number 

 Config :: 

   server.modules += ( "mod_magnet" ) 
   magnet.attract-physical-path-to = ("random.lua") 

 random.lua :: 

   dir = lighty.env["physical.path"] 

   f = assert(io.open(dir .. "/index", "r")) 
   maxndx = f:read("*all") 
   f:close() 

   ndx = math.random(maxndx) 

   lighty.content = { { filename = dir .. "/" .. ndx }} 
   lighty.header["Content-Type"] = "image/png" 

   return 200 

 denying illegal character sequences in the URL 
 ---------------------------------------------- 

 Instead of implementing mod_security, you might just want to apply filters on the content 
 and deny special sequences that look like SQL injection.  

 A common injection is using UNION to extend a query with another SELECT query. 

 :: 

   if (string.find(lighty.env["request.uri"], "UNION%s")) then 
     return 400 
   end 

 Traffic Quotas 
 -------------- 

 If you only allow your virtual hosts a certain amount for traffic each month and want to  
 disable them if the traffic is reached, perhaps this helps: :: 

   host_blacklist = { ["www.example.org"] = 0 } 

   if (host_blacklist[lighty.request["Host"]]) then 
     return 404 
   end 

 Just add the hosts you want to blacklist into the blacklist table in the shown way. 

 Complex rewrites 
 ---------------- 

 If you want to implement caching on your document-root and only want to regenerate  
 content if the requested file doesn't exist, you can attract the physical.path: :: 

   magnet.attract-physical-path-to = ( server.document-root + "/rewrite.lua" ) 

 rewrite.lua :: 

   attr = lighty.stat(lighty.env["physical.path"]) 

   if (not attr) then 
     -- we couldn't stat() the file for some reason 
     -- let the backend generate it 

     lighty.env["uri.path"] = "/dispatch.fcgi" 
     lighty.env["physical.rel-path"] = lighty.env["uri.path"] 
     lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"] 
   end 

 Extention rewrites 
 ------------------ 

 If you want to hide your file extentions (like .php) you can attract the physical.path: :: 

   magnet.attract-physical-path-to = ( server.document-root + "/rewrite.lua" ) 

 rewrite.lua :: 

   attr = lighty.stat(lighty.env["physical.path"] .. ".php") 

   if (attr) then 
     lighty.env["uri.path"] = lighty.env["uri.path"] .. ".php" 
     lighty.env["physical.rel-path"] = lighty.env["uri.path"] 
     lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"] 
   end 

 Usertracking 
 ------------ 

 ... or how to store data globally in the script-context: 

 Each script has its own script-context. When the script is started it only contains the lua-functions 
 and the special lighty.* name-space. If you want to save data between script runs, you can use the global-script 
 context: 

 :: 

   if (nil == _G["usertrack"]) then 
     _G["usertrack"] = {} 
   end 
   if (nil == _G["usertrack"][lighty.request["Cookie"]]) then 
     _G["usertrack"][lighty.request["Cookie"]] 
   else  
     _G["usertrack"][lighty.request["Cookie"]] = _G["usertrack"][lighty.request["Cookie"]] + 1 
   end 

   print _G["usertrack"][lighty.request["Cookie"]] 

 The global-context is per script. If you update the script without restarting the server, the context will still be maintained. 

 Counters 
 -------- 

 mod_status support a global statistics page and mod_magnet allows to add and update values in the status page: 

 Config :: 

   status.statistics-url = "/server-counters" 
   magnet.attract-raw-url-to = (server.docroot + "/counter.lua") 

 counter.lua :: 

   lighty.status["core.connections"] = lighty.status["core.connections"] + 1 

 Result:: 

   core.connections: 7 
   fastcgi.backend.php-foo.0.connected: 0 
   fastcgi.backend.php-foo.0.died: 0 
   fastcgi.backend.php-foo.0.disabled: 0 
   fastcgi.backend.php-foo.0.load: 0 
   fastcgi.backend.php-foo.0.overloaded: 0 
   fastcgi.backend.php-foo.1.connected: 0 
   fastcgi.backend.php-foo.1.died: 0 
   fastcgi.backend.php-foo.1.disabled: 0 
   fastcgi.backend.php-foo.1.load: 0 
   fastcgi.backend.php-foo.1.overloaded: 0 
   fastcgi.backend.php-foo.load: 0 

 Porting mod_cml scripts 
 ----------------------- 

 mod_cml got replaced by mod_magnet. 

 A CACHE_HIT in mod_cml:: 
 
   output_include = { "file1", "file2" }  

   return CACHE_HIT 

 becomes:: 

   content = { { filename = "/path/to/file1" }, { filename = "/path/to/file2"} } 

   return 200 

 while a CACHE_MISS like (CML) :: 

   trigger_handler = "/index.php" 

   return CACHE_MISS 

 becomes (magnet) :: 

   lighty.env["request.uri"] = "/index.php" 

   return lighty.RESTART_REQUEST 

 }}}