Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Content Negotiation Lua Script for nginx
-- 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)
-- ©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
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
You can’t perform that action at this time.