From a63fa963b7bd01f1b470bea7c5a2ed326ff760e6 Mon Sep 17 00:00:00 2001
From: Thijs Schreijer
-You can obtain the headers namespace if case run-time -modifications are required by running: +If a lowercase field name is not present as a key, +socket.headers.canonic automatically derives a +capitalization by uppercasing the first letter of each +hyphen-separated word (e.g. x-request-id becomes +X-Request-Id), and caches the result for later lookups. +A small set of headers whose canonic capitalization is +irregular (such as ETag, Content-MD5, +MIME-Version and WWW-Authenticate) are kept as +explicit entries in the table and are not affected by this rule. +Because of this, pairs(socket.headers.canonic) only +shows the irregular entries plus any field name already looked +up or added — it is not an exhaustive list of every header +name the library knows how to capitalize. +
+ ++You can register a custom canonic capitalization — for a +header the automatic rule would not reproduce, such as a custom +all-caps acronym — with socket.headers.setcanonic:
-- loads the headers module
-local headers = require("headers")
+local headers = require("socket.headers")
+
+-- register a custom capitalization
+headers.setcanonic("X-Request-ID")
diff --git a/src/headers.lua b/src/headers.lua
index 1eb8223..f8b8ef8 100644
--- a/src/headers.lua
+++ b/src/headers.lua
@@ -7,98 +7,55 @@ local socket = require("socket")
socket.headers = {}
local _M = socket.headers
-_M.canonic = {
- ["accept"] = "Accept",
- ["accept-charset"] = "Accept-Charset",
- ["accept-encoding"] = "Accept-Encoding",
- ["accept-language"] = "Accept-Language",
- ["accept-ranges"] = "Accept-Ranges",
- ["action"] = "Action",
- ["alternate-recipient"] = "Alternate-Recipient",
- ["age"] = "Age",
- ["allow"] = "Allow",
- ["arrival-date"] = "Arrival-Date",
- ["authorization"] = "Authorization",
- ["bcc"] = "Bcc",
- ["cache-control"] = "Cache-Control",
- ["cc"] = "Cc",
- ["comments"] = "Comments",
- ["connection"] = "Connection",
- ["content-description"] = "Content-Description",
- ["content-disposition"] = "Content-Disposition",
- ["content-encoding"] = "Content-Encoding",
- ["content-id"] = "Content-ID",
- ["content-language"] = "Content-Language",
- ["content-length"] = "Content-Length",
- ["content-location"] = "Content-Location",
- ["content-md5"] = "Content-MD5",
- ["content-range"] = "Content-Range",
- ["content-transfer-encoding"] = "Content-Transfer-Encoding",
- ["content-type"] = "Content-Type",
- ["cookie"] = "Cookie",
- ["date"] = "Date",
- ["diagnostic-code"] = "Diagnostic-Code",
- ["dsn-gateway"] = "DSN-Gateway",
- ["etag"] = "ETag",
- ["expect"] = "Expect",
- ["expires"] = "Expires",
- ["final-log-id"] = "Final-Log-ID",
- ["final-recipient"] = "Final-Recipient",
- ["from"] = "From",
- ["host"] = "Host",
- ["if-match"] = "If-Match",
- ["if-modified-since"] = "If-Modified-Since",
- ["if-none-match"] = "If-None-Match",
- ["if-range"] = "If-Range",
- ["if-unmodified-since"] = "If-Unmodified-Since",
- ["in-reply-to"] = "In-Reply-To",
- ["keywords"] = "Keywords",
- ["last-attempt-date"] = "Last-Attempt-Date",
- ["last-modified"] = "Last-Modified",
- ["location"] = "Location",
- ["max-forwards"] = "Max-Forwards",
- ["message-id"] = "Message-ID",
- ["mime-version"] = "MIME-Version",
- ["original-envelope-id"] = "Original-Envelope-ID",
- ["original-recipient"] = "Original-Recipient",
- ["pragma"] = "Pragma",
- ["proxy-authenticate"] = "Proxy-Authenticate",
- ["proxy-authorization"] = "Proxy-Authorization",
- ["range"] = "Range",
- ["received"] = "Received",
- ["received-from-mta"] = "Received-From-MTA",
- ["references"] = "References",
- ["referer"] = "Referer",
- ["remote-mta"] = "Remote-MTA",
- ["reply-to"] = "Reply-To",
- ["reporting-mta"] = "Reporting-MTA",
- ["resent-bcc"] = "Resent-Bcc",
- ["resent-cc"] = "Resent-Cc",
- ["resent-date"] = "Resent-Date",
- ["resent-from"] = "Resent-From",
- ["resent-message-id"] = "Resent-Message-ID",
- ["resent-reply-to"] = "Resent-Reply-To",
- ["resent-sender"] = "Resent-Sender",
- ["resent-to"] = "Resent-To",
- ["retry-after"] = "Retry-After",
- ["return-path"] = "Return-Path",
- ["sender"] = "Sender",
- ["server"] = "Server",
- ["smtp-remote-recipient"] = "SMTP-Remote-Recipient",
- ["status"] = "Status",
- ["subject"] = "Subject",
- ["te"] = "TE",
- ["to"] = "To",
- ["trailer"] = "Trailer",
- ["transfer-encoding"] = "Transfer-Encoding",
- ["upgrade"] = "Upgrade",
- ["user-agent"] = "User-Agent",
- ["vary"] = "Vary",
- ["via"] = "Via",
- ["warning"] = "Warning",
- ["will-retry-until"] = "Will-Retry-Until",
- ["www-authenticate"] = "WWW-Authenticate",
- ["x-mailer"] = "X-Mailer",
-}
+-- capitalizes the first letter of each hyphen-separated word, lowercases
+-- the rest (e.g. "x-request-id" -> "X-Request-Id")
+local function titlecase(header)
+ return (header:gsub("(%a)([%w]*)", function(a, b) return a:upper()..b:lower() end))
+end
-return _M
\ No newline at end of file
+_M.canonic = {}
+
+setmetatable(_M.canonic, {
+ __index = function(t, key)
+ if type(key) ~= "string" then
+ return nil
+ end
+
+ local lower = key:lower()
+ local v = rawget(t, lower)
+ if v then
+ return v
+ end
+
+ v = titlecase(lower)
+ rawset(t, lower, v)
+ return v
+ end
+})
+
+-- adds a header with a given canonical capitalization, e.g. for headers
+-- whose capitalization titlecase(header) would not reproduce correctly
+function _M.setcanonic(header)
+ _M.canonic[header:lower()] = header
+end
+
+-- headers whose canonical capitalization titlecase() does not reproduce
+-- (acronyms and other irregular capitalization). Anything not listed here
+-- is generated and cached on first lookup by the __index above.
+_M.setcanonic("Content-ID")
+_M.setcanonic("Content-MD5")
+_M.setcanonic("DSN-Gateway")
+_M.setcanonic("ETag")
+_M.setcanonic("Final-Log-ID")
+_M.setcanonic("Message-ID")
+_M.setcanonic("MIME-Version")
+_M.setcanonic("Original-Envelope-ID")
+_M.setcanonic("Received-From-MTA")
+_M.setcanonic("Remote-MTA")
+_M.setcanonic("Reporting-MTA")
+_M.setcanonic("Resent-Message-ID")
+_M.setcanonic("SMTP-Remote-Recipient")
+_M.setcanonic("TE")
+_M.setcanonic("WWW-Authenticate")
+
+return _M
diff --git a/test/headerstest.lua b/test/headerstest.lua
new file mode 100644
index 0000000..1387fc7
--- /dev/null
+++ b/test/headerstest.lua
@@ -0,0 +1,131 @@
+local socket = require("socket")
+local headers = require("socket.headers")
+
+-- the original hardcoded table this file used to contain, kept here so the
+-- refactor to dynamic generation + a small anomaly table can be checked for
+-- regressions.
+local canonic = {
+ ["accept"] = "Accept",
+ ["accept-charset"] = "Accept-Charset",
+ ["accept-encoding"] = "Accept-Encoding",
+ ["accept-language"] = "Accept-Language",
+ ["accept-ranges"] = "Accept-Ranges",
+ ["action"] = "Action",
+ ["alternate-recipient"] = "Alternate-Recipient",
+ ["age"] = "Age",
+ ["allow"] = "Allow",
+ ["arrival-date"] = "Arrival-Date",
+ ["authorization"] = "Authorization",
+ ["bcc"] = "Bcc",
+ ["cache-control"] = "Cache-Control",
+ ["cc"] = "Cc",
+ ["comments"] = "Comments",
+ ["connection"] = "Connection",
+ ["content-description"] = "Content-Description",
+ ["content-disposition"] = "Content-Disposition",
+ ["content-encoding"] = "Content-Encoding",
+ ["content-id"] = "Content-ID",
+ ["content-language"] = "Content-Language",
+ ["content-length"] = "Content-Length",
+ ["content-location"] = "Content-Location",
+ ["content-md5"] = "Content-MD5",
+ ["content-range"] = "Content-Range",
+ ["content-transfer-encoding"] = "Content-Transfer-Encoding",
+ ["content-type"] = "Content-Type",
+ ["cookie"] = "Cookie",
+ ["date"] = "Date",
+ ["diagnostic-code"] = "Diagnostic-Code",
+ ["dsn-gateway"] = "DSN-Gateway",
+ ["etag"] = "ETag",
+ ["expect"] = "Expect",
+ ["expires"] = "Expires",
+ ["final-log-id"] = "Final-Log-ID",
+ ["final-recipient"] = "Final-Recipient",
+ ["from"] = "From",
+ ["host"] = "Host",
+ ["if-match"] = "If-Match",
+ ["if-modified-since"] = "If-Modified-Since",
+ ["if-none-match"] = "If-None-Match",
+ ["if-range"] = "If-Range",
+ ["if-unmodified-since"] = "If-Unmodified-Since",
+ ["in-reply-to"] = "In-Reply-To",
+ ["keywords"] = "Keywords",
+ ["last-attempt-date"] = "Last-Attempt-Date",
+ ["last-modified"] = "Last-Modified",
+ ["location"] = "Location",
+ ["max-forwards"] = "Max-Forwards",
+ ["message-id"] = "Message-ID",
+ ["mime-version"] = "MIME-Version",
+ ["original-envelope-id"] = "Original-Envelope-ID",
+ ["original-recipient"] = "Original-Recipient",
+ ["pragma"] = "Pragma",
+ ["proxy-authenticate"] = "Proxy-Authenticate",
+ ["proxy-authorization"] = "Proxy-Authorization",
+ ["range"] = "Range",
+ ["received"] = "Received",
+ ["received-from-mta"] = "Received-From-MTA",
+ ["references"] = "References",
+ ["referer"] = "Referer",
+ ["remote-mta"] = "Remote-MTA",
+ ["reply-to"] = "Reply-To",
+ ["reporting-mta"] = "Reporting-MTA",
+ ["resent-bcc"] = "Resent-Bcc",
+ ["resent-cc"] = "Resent-Cc",
+ ["resent-date"] = "Resent-Date",
+ ["resent-from"] = "Resent-From",
+ ["resent-message-id"] = "Resent-Message-ID",
+ ["resent-reply-to"] = "Resent-Reply-To",
+ ["resent-sender"] = "Resent-Sender",
+ ["resent-to"] = "Resent-To",
+ ["retry-after"] = "Retry-After",
+ ["return-path"] = "Return-Path",
+ ["sender"] = "Sender",
+ ["server"] = "Server",
+ ["smtp-remote-recipient"] = "SMTP-Remote-Recipient",
+ ["status"] = "Status",
+ ["subject"] = "Subject",
+ ["te"] = "TE",
+ ["to"] = "To",
+ ["trailer"] = "Trailer",
+ ["transfer-encoding"] = "Transfer-Encoding",
+ ["upgrade"] = "Upgrade",
+ ["user-agent"] = "User-Agent",
+ ["vary"] = "Vary",
+ ["via"] = "Via",
+ ["warning"] = "Warning",
+ ["will-retry-until"] = "Will-Retry-Until",
+ ["www-authenticate"] = "WWW-Authenticate",
+ ["x-mailer"] = "X-Mailer",
+}
+
+local check_canonic = function(lower, expected)
+ local got = headers.canonic[lower]
+ if got ~= expected then
+ print("canonic[" .. lower .. "] = " .. tostring(got) ..
+ ", expected " .. expected)
+ os.exit()
+ end
+end
+
+print("testing all known header fields resolve to their canonical form")
+for lower, expected in pairs(canonic) do
+ check_canonic(lower, expected)
+end
+
+print("testing mixed/upper-case input normalizes correctly")
+check_canonic("Content-Type", "Content-Type")
+check_canonic("CONTENT-TYPE", "Content-Type")
+check_canonic("ETAG", "ETag")
+check_canonic("Www-Authenticate", "WWW-Authenticate")
+
+print("testing novel unregistered headers are auto-titlecased")
+check_canonic("x-request-id", "X-Request-Id")
+check_canonic("x-custom-header-name", "X-Custom-Header-Name")
+check_canonic("single", "Single")
+
+print("testing setcanonic() registers a custom canonical capitalization")
+headers.setcanonic("X-Request-ID")
+check_canonic("x-request-id", "X-Request-ID")
+check_canonic("X-REQUEST-ID", "X-Request-ID")
+
+print("the library passed all tests")
--
cgit v1.2.3-55-g6feb