Last active
December 22, 2015 08:49
-
-
Save BlameOmar/6447669 to your computer and use it in GitHub Desktop.
Content Negotiation Lua Script for nginx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Content negotiation for nginx using the lua module | |
-- Version 0.2 | |
-- ©2013 Omar Stefan Evans | |
-- Based on the content negotiation script for Lighttpd written by Michael Gorven | |
-- (http://michael.gorven.za.net/blog/2009/04/13/content-negotiation-lighttpd-lua) | |
-- It is licensed under a BSD license. | |
-- Permission is hereby granted, free of charge, to any person obtaining a copy of | |
-- this software and associated documentation files (the "Software"), to deal in | |
-- the Software without restriction, including without limitation the rights to | |
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
-- of the Software, and to permit persons to whom the Software is furnished to do | |
-- so, subject to the following conditions: | |
-- | |
-- The above copyright notice and this permission notice shall be included in all | |
-- copies or substantial portions of the Software. | |
-- | |
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
-- SOFTWARE. | |
-- Preconditions: | |
-- (1) try_files failed for $uri and $uri/ | |
-- (2) ngx.shared.types and ngx.shared.mime_type_scores have been created and | |
-- have been set. See init.lua | |
-- Caveats: | |
-- (1) Currently only negotiates MIME types, not languages | |
local path = ngx.var.uri | |
-- path must not refer to a directory | |
if path:match("[/]$") then | |
return ngx.exit(404) | |
end | |
local resource = path:match("[^/]*$") | |
local directory = path:sub(1, path:len() - resource:len()) | |
local directory_fspath = ngx.var.document_root..directory | |
local lfs = require "lfs" | |
if lfs.attributes(directory_fspath, "mode") ~= "directory" then | |
return ngx.exit(404) | |
end | |
local variants = {} | |
local variant_count = 0 | |
for filename in lfs.dir(directory_fspath) do | |
if filename:match(resource.."%.+") then | |
local mime_type = ngx.shared.types:get(filename:match(resource.."%.(.+)")) or "application/octet-stream" | |
variants[mime_type] = filename | |
variant_count = variant_count + 1 | |
end | |
end | |
if variant_count == 0 then | |
return ngx.exit(404) | |
end | |
-- Parse Accept header | |
local accepted_mimetype_scores = {} | |
if not ngx.req.get_headers()["Accept"] then | |
accepted_mimetype_scores["*/*"] = 1.0 | |
else | |
for range in ngx.req.get_headers()["Accept"]:gmatch(" *([^,]+) *") do | |
accepted_mimetype_scores[range:match("([^;]+)")] = range:match("q *= *([0-9.]+)") or 1.0 | |
end | |
end | |
-- Hack for user agents which don't send quality values for wildcards | |
if ngx.req.get_headers()["Accept"] and not ngx.req.get_headers()["Accept"]:find("q=") then | |
for mimetype, score in pairs(accepted_mimetype_scores) do | |
if mimetype == "*/*" then | |
accepted_mimetype_scores[mimetype] = 0.01 | |
elseif mimetype:find("*") then | |
accepted_mimetype_scores[mimetype] = 0.02 | |
end | |
end | |
end | |
-- Calculate the scores of each variant | |
local variant_scores = {} | |
for mimetype, filename in pairs(variants) do | |
s1 = accepted_mimetype_scores[mimetype] or accepted_mimetype_scores["*/*"] or 0.0 | |
s2 = ngx.shared.mime_type_scores:get(mimetype) or ngx.shared.mime_type_scores:get("*/*") or 1.0 | |
variant_scores[filename] = s1 * s2 | |
end | |
-- Choose the variant with the highest score | |
local highscore = 0.0 | |
local choice = "" | |
for filename, score in pairs(variant_scores) do | |
if score > highscore then | |
highscore = score | |
choice = filename | |
elseif score == highscore and filename < choice then | |
choice = filename | |
end | |
end | |
-- Return '406 Not Acceptable' if no variants match | |
if choice == "" then | |
local html_page_header = [[ | |
<!DOCTYPE html> | |
<html> | |
<head><title>406 Not Acceptable</title></head> | |
<body style='text-align: center;'> | |
<h1>406 Not Acceptable</h1> | |
<p>The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. Alternative content may be available.</p> | |
<ul> | |
]] | |
local html_page_footer = [[ | |
</ul> | |
</body> | |
</html> | |
]] | |
ngx.status = 406 | |
ngx.header.content_type = "text/html; charset=utf-8" | |
ngx.print(html_page_header) | |
for mimetype, filename in pairs(variants) do | |
ngx.print("<li><a href='"..directory..filename.."'>"..directory..filename.."("..mimetype..")</a></li>\n") | |
end | |
ngx.print(html_page_footer) | |
end | |
-- Try to make caches do the right thing | |
local http_version = ngx.req.http_version() | |
if http_version >= 1.1 then | |
ngx.header.vary = "Accept" | |
else | |
ngx.header.expires = "Thu, 01 Jan 1970 00:00:00 GMT" | |
end | |
local res = ngx.location.capture(directory..choice) | |
ngx.status = res.status | |
for k,v in pairs(res.header) do | |
ngx.header[k] = v | |
end | |
ngx.print(res.body) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- ©2013 Omar Stefan Evans | |
-- Based on the content negotiation script for Lighttpd written by Michael Gorven | |
-- (http://michael.gorven.za.net/blog/2009/04/13/content-negotiation-lighttpd-lua) | |
-- It is licensed under a BSD license. | |
-- Permission is hereby granted, free of charge, to any person obtaining a copy of | |
-- this software and associated documentation files (the "Software"), to deal in | |
-- the Software without restriction, including without limitation the rights to | |
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
-- of the Software, and to permit persons to whom the Software is furnished to do | |
-- so, subject to the following conditions: | |
-- | |
-- The above copyright notice and this permission notice shall be included in all | |
-- copies or substantial portions of the Software. | |
-- | |
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
-- SOFTWARE. | |
function load_nginx_mime_types(file_absolute_path) | |
local types = {} | |
local f = io.open(file_absolute_path) | |
local t = "" | |
if f ~= nil then | |
t = f:read("*all") | |
end | |
io.close(f) | |
t = t:match("types%s+{(.-)}") | |
t = t:gsub("#.-\n", "\n") | |
for s in t:gmatch("%s*([^;]+)%s*") do | |
local mimetype = s:match("[%S]+/[%S]+") | |
if mimetype ~= nil then | |
for extension in s:sub(mimetype:len() + 1):gmatch("%S+") do | |
types[extension] = mimetype | |
end | |
end | |
end | |
return types | |
end | |
local mime_type_scores = { | |
["application/xhtml+xml"] = 1.0, | |
["text/html"] = 0.99, | |
["application/xml"] = 0.98, | |
["text/plain"] = 0.95, | |
["image/svg+xml"] = 1.0, | |
["image/png"] = 0.99, | |
["image/gif"] = 0.98, | |
["image/jpeg"] = 0.98, | |
["*/*"] = 0.9, | |
} | |
for k,v in pairs(load_nginx_mime_types("/etc/nginx/mime.types")) do | |
ngx.shared.types:set(k, v) | |
end | |
local scores = ngx.shared.mime_type_scores | |
for k,v in pairs(mime_type_scores) do | |
ngx.shared.mime_type_scores:set(k, v) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
http { | |
... | |
lua_shared_dict types 10m; | |
lua_shared_dict mime_type_scores 10m; | |
init_by_lua_file lua/init.lua; | |
... | |
location / { | |
try_files $uri $uri/ @content_negotiation; | |
} | |
location @content_negotiation { | |
content_by_lua_file lua/content_negotiation.lua; | |
} | |
... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment