diff --git a/.luacheckrc b/.luacheckrc
index b1f4e2e..73ef470 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -1,2 +1,3 @@
 std = "min"
 files["spec"] = {std = "+busted"}
+max_line_length = false
diff --git a/NEWS b/NEWS
index aaeb806..5f6cd5d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,17 @@
+UNRELEASED
+
+  - http: split into multiple modules
+  - http.alpn: verify that protocol_id meets unique encoding criteria
+
+
+0.5 - 2018-07-15
+
+  - http: Cache-Control directives are case-normalised and grouped into pairs
+  - http: Strict_Transport_Security now returns a table and doesn't match on duplicates
+  - http: Public_Key_Pins capture format and validation
+  - http: New Expect_CT and Referrer_Policy patterns
+
+
 0.4 - 2016-11-23
 
   - Reduce memory usage by refactoring IPv6 pattern
diff --git a/README.md b/README.md
index 332d7f9..410e0d4 100644
--- a/README.md
+++ b/README.md
@@ -258,7 +258,7 @@ These patterns should be considered to have non stable APIs.
 #### [RFC 7234](https://tools.ietf.org/html/rfc7234)
 
   - `Age` (pattern)
-  - `Cache_Control` (pattern)
+  - `Cache_Control` (pattern): captures are grouped into key/value pairs (where a directive with no value has a value of `true`)
   - `Expires` (pattern): capture is a table in the same format as used by [`os.time`](http://www.lua.org/manual/5.3/manual.html#pdf-os.time)
   - `Pragma` (pattern)
   - `Warning` (pattern)
@@ -310,6 +310,16 @@ These patterns should be considered to have non stable APIs.
   - `Alt_Used` (pattern)
 
 
+#### [Expect-CT Extension for HTTP](https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-06)
+
+  - `Expect_CT` (pattern)
+
+
+#### [Referrer-Policy header](https://www.w3.org/TR/referrer-policy/#referrer-policy-header)
+
+  - `Referrer_Policy` (pattern)
+
+
 ### `phone`
 
   - `phone` (pattern): includes detailed checking for:
diff --git a/lpeg_patterns-0.4-0.rockspec b/lpeg_patterns-0.4-0.rockspec
deleted file mode 100644
index 836d8b9..0000000
--- a/lpeg_patterns-0.4-0.rockspec
+++ /dev/null
@@ -1,32 +0,0 @@
-package = "lpeg_patterns"
-version = "0.4-0"
-
-description= {
-	summary = "a collection of LPEG patterns";
-	license = "MIT";
-}
-
-dependencies = {
-	"lua";
-	"lpeg";
-}
-
-source = {
-	url = "https://github.com/daurnimator/lpeg_patterns/archive/v0.4.zip";
-	dir = "lpeg_patterns-0.4";
-}
-
-build = {
-	type = "builtin";
-	modules = {
-		["lpeg_patterns.util"] = "lpeg_patterns/util.lua";
-		["lpeg_patterns.core"] = "lpeg_patterns/core.lua";
-		["lpeg_patterns.IPv4"] = "lpeg_patterns/IPv4.lua";
-		["lpeg_patterns.IPv6"] = "lpeg_patterns/IPv6.lua";
-		["lpeg_patterns.uri"] = "lpeg_patterns/uri.lua";
-		["lpeg_patterns.email"] = "lpeg_patterns/email.lua";
-		["lpeg_patterns.http"] = "lpeg_patterns/http.lua";
-		["lpeg_patterns.phone"] = "lpeg_patterns/phone.lua";
-		["lpeg_patterns.language"] = "lpeg_patterns/language.lua";
-	};
-}
diff --git a/lpeg_patterns-scm-0.rockspec b/lpeg_patterns-scm-0.rockspec
new file mode 100644
index 0000000..a68995e
--- /dev/null
+++ b/lpeg_patterns-scm-0.rockspec
@@ -0,0 +1,55 @@
+package = "lpeg_patterns"
+version = "scm-0"
+
+description= {
+	summary = "a collection of LPEG patterns";
+	license = "MIT";
+}
+
+dependencies = {
+	"lua";
+	"lpeg";
+}
+
+source = {
+	url = "git://github.com/daurnimator/lpeg_patterns.git";
+}
+
+build = {
+	type = "builtin";
+	modules = {
+		["lpeg_patterns.util"] = "lpeg_patterns/util.lua";
+		["lpeg_patterns.core"] = "lpeg_patterns/core.lua";
+		["lpeg_patterns.IPv4"] = "lpeg_patterns/IPv4.lua";
+		["lpeg_patterns.IPv6"] = "lpeg_patterns/IPv6.lua";
+		["lpeg_patterns.uri"] = "lpeg_patterns/uri.lua";
+		["lpeg_patterns.email"] = "lpeg_patterns/email.lua";
+		["lpeg_patterns.http"] = "lpeg_patterns/http.lua";
+		["lpeg_patterns.http.alpn"] = "lpeg_patterns/http/alpn.lua";
+		["lpeg_patterns.http.alternate"] = "lpeg_patterns/http/alternate.lua";
+		["lpeg_patterns.http.authentication"] = "lpeg_patterns/http/authentication.lua";
+		["lpeg_patterns.http.caching"] = "lpeg_patterns/http/caching.lua";
+		["lpeg_patterns.http.conditional"] = "lpeg_patterns/http/conditional.lua";
+		["lpeg_patterns.http.cookie"] = "lpeg_patterns/http/cookie.lua";
+		["lpeg_patterns.http.core"] = "lpeg_patterns/http/core.lua";
+		["lpeg_patterns.http.disposition"] = "lpeg_patterns/http/disposition.lua";
+		["lpeg_patterns.http.expect_ct"] = "lpeg_patterns/http/expect_ct.lua";
+		["lpeg_patterns.http.forwarded"] = "lpeg_patterns/http/forwarded.lua";
+		["lpeg_patterns.http.frameoptions"] = "lpeg_patterns/http/frameoptions.lua";
+		["lpeg_patterns.http.hoba"] = "lpeg_patterns/http/hoba.lua";
+		["lpeg_patterns.http.link"] = "lpeg_patterns/http/link.lua";
+		["lpeg_patterns.http.origin"] = "lpeg_patterns/http/origin.lua";
+		["lpeg_patterns.http.parameters"] = "lpeg_patterns/http/parameters.lua";
+		["lpeg_patterns.http.pkp"] = "lpeg_patterns/http/pkp.lua";
+		["lpeg_patterns.http.range"] = "lpeg_patterns/http/range.lua";
+		["lpeg_patterns.http.referrer_policy"] = "lpeg_patterns/http/referrer_policy.lua";
+		["lpeg_patterns.http.semantics"] = "lpeg_patterns/http/semantics.lua";
+		["lpeg_patterns.http.slug"] = "lpeg_patterns/http/slug.lua";
+		["lpeg_patterns.http.sts"] = "lpeg_patterns/http/sts.lua";
+		["lpeg_patterns.http.util"] = "lpeg_patterns/http/util.lua";
+		["lpeg_patterns.http.webdav"] = "lpeg_patterns/http/webdav.lua";
+		["lpeg_patterns.http.websocket"] = "lpeg_patterns/http/websocket.lua";
+		["lpeg_patterns.phone"] = "lpeg_patterns/phone.lua";
+		["lpeg_patterns.language"] = "lpeg_patterns/language.lua";
+	};
+}
diff --git a/lpeg_patterns/IPv6.lua b/lpeg_patterns/IPv6.lua
index 314c0a9..b4ab116 100644
--- a/lpeg_patterns/IPv6.lua
+++ b/lpeg_patterns/IPv6.lua
@@ -1,6 +1,6 @@
 -- IPv6
 
-local unpack = table.unpack or unpack -- luacheck: ignore 113
+local unpack = table.unpack or unpack -- luacheck: ignore 113 143
 
 local lpeg = require "lpeg"
 local P = lpeg.P
diff --git a/lpeg_patterns/http.lua b/lpeg_patterns/http.lua
index 3f7fa9d..4a9e11b 100644
--- a/lpeg_patterns/http.lua
+++ b/lpeg_patterns/http.lua
@@ -1,597 +1,171 @@
---[[
-https://tools.ietf.org/html/rfc7230
-https://tools.ietf.org/html/rfc7231
-]]
-
-local lpeg = require "lpeg"
-local core = require "lpeg_patterns.core"
-local email = require "lpeg_patterns.email"
-local language = require "lpeg_patterns.language"
-local uri = require "lpeg_patterns.uri"
-local util = require "lpeg_patterns.util"
-
-local C = lpeg.C
-local Cc = lpeg.Cc
-local Cf = lpeg.Cf
-local Cg = lpeg.Cg
-local Cs = lpeg.Cs
-local Ct = lpeg.Ct
-local Cmt = lpeg.Cmt
-local P = lpeg.P
-local R = lpeg.R
-local S = lpeg.S
-local V = lpeg.V
+-- HTTP related patterns
 
 local _M = {}
 
-local T_F = S"Tt" * Cc(true) + S"Ff" * Cc(false)
-
-local function no_rich_capture(patt)
-	return C(patt) / function(a) return a end
-end
+-- RFC 7230
+local http_core = require "lpeg_patterns.http.core"
+_M.OWS = http_core.OWS
+_M.RWS = http_core.RWS
+_M.BWS = http_core.BWS
+
+_M.chunk_ext = http_core.chunk_ext
+_M.comment = http_core.comment
+_M.field_name = http_core.field_name
+_M.field_value = http_core.field_value
+_M.header_field = http_core.header_field
+_M.qdtext = http_core.qdtext
+_M.quoted_string = http_core.quoted_string
+_M.request_line = http_core.request_line
+_M.request_target = http_core.request_target
+_M.token = http_core.token
+
+_M.Connection = http_core.Connection
+_M.Content_Length = http_core.Content_Length
+_M.Host = http_core.Host
+_M.TE = http_core.TE
+_M.Trailer = http_core.Trailer
+_M.Transfer_Encoding = http_core.Transfer_Encoding
+_M.Upgrade = http_core.Upgrade
+_M.Via = http_core.Via
+
+-- RFC 7231
+local http_semantics = require "lpeg_patterns.http.semantics"
+
+_M.IMF_fixdate = http_semantics.IMF_fixdate
+
+_M.Accept = http_semantics.Accept
+_M.Accept_Charset = http_semantics.Accept_Charset
+_M.Accept_Encoding = http_semantics.Accept_Encoding
+_M.Accept_Language = http_semantics.Accept_Language
+_M.Allow = http_semantics.Allow
+_M.Content_Encoding = http_semantics.Content_Encoding
+_M.Content_Language = http_semantics.Content_Language
+_M.Content_Location = http_semantics.Content_Location
+_M.Content_Type = http_semantics.Content_Type
+_M.Date = http_semantics.Date
+_M.Expect = http_semantics.Expect
+_M.From = http_semantics.From
+_M.Location = http_semantics.Location
+_M.Max_Forwards = http_semantics.Max_Forwards
+_M.Referer = http_semantics.Referer
+_M.Retry_After = http_semantics.Retry_After
+_M.Server = http_semantics.Server
+_M.User_Agent = http_semantics.User_Agent
+_M.Vary = http_semantics.Vary
+
+-- RFC 7232
+local http_conditional = require "lpeg_patterns.http.conditional"
+_M.ETag = http_conditional.ETag
+_M.If_Match = http_conditional.If_Match
+_M.If_Modified_Since = http_conditional.If_Modified_Since
+_M.If_None_Match = http_conditional.If_None_Match
+_M.If_Unmodified_Since = http_conditional.If_Unmodified_Since
+_M.Last_Modified = http_conditional.Last_Modified
 
-local function case_insensitive(str)
-	local patt = P(true)
-	for i=1, #str do
-		local c = str:sub(i, i)
-		patt = patt * S(c:upper() .. c:lower())
-	end
-	return patt
-end
-
--- RFC 7230 Section 3.2.3
-_M.OWS = (core.SP + core.HTAB)^0
-_M.RWS = (core.SP + core.HTAB)^1
-_M.BWS = _M.OWS
+-- RFC 7233
+local http_range = require "lpeg_patterns.http.range"
+_M.Accept_Ranges = http_range.Accept_Ranges
+_M.Range = http_range.Range
+_M.If_Range = http_range.If_Range
+_M.Content_Range = http_range.Content_Range
+
+-- RFC 7234
+local http_caching = require "lpeg_patterns.http.caching"
+_M.Age = http_caching.Age
+_M.Cache_Control = http_caching.Cache_Control
+_M.Expires = http_caching.Expires
+_M.Pragma = http_caching.Pragma
+_M.Warning = http_caching.Warning
+
+-- RFC 7235
+local http_authentication = require "lpeg_patterns.http.authentication"
+_M.WWW_Authenticate = http_authentication.WWW_Authenticate
+_M.Authorization = http_authentication.Authorization
+_M.Proxy_Authenticate = http_authentication.Proxy_Authenticate
+_M.Proxy_Authorization = http_authentication.Proxy_Authorization
+
+-- WebDav
+local http_webdav = require "lpeg_patterns.http.webdav"
+_M.CalDAV_Timezones = http_webdav.CalDAV_Timezones
+_M.DASL = http_webdav.DASL
+_M.DAV = http_webdav.DAV
+_M.Depth = http_webdav.Depth
+_M.Destination = http_webdav.Destination
+_M.If = http_webdav.If
+_M.If_Schedule_Tag_Match = http_webdav.If_Schedule_Tag_Match
+_M.Lock_Token = http_webdav.Lock_Token
+_M.Overwrite = http_webdav.Overwrite
+_M.Schedule_Reply = http_webdav.Schedule_Reply
+_M.Schedule_Tag = http_webdav.Schedule_Tag
+_M.TimeOut = http_webdav.TimeOut
 
 -- RFC 5023
-local slugtext = _M.RWS / " "
-	+ P"%" * (core.HEXDIG * core.HEXDIG / util.read_hex) / string.char
-	+ R"\32\126"
-_M.SLUG = Cs(slugtext^0)
-
--- RFC 6454
--- discard captures from scheme, host, port and just get whole string
-local serialized_origin = C(uri.scheme * P"://" * uri.host * (P":" * uri.port)^-1/function() end)
-local origin_list = serialized_origin * (core.SP * serialized_origin)^0
-local origin_list_or_null = P"null" + origin_list
-_M.Origin = _M.OWS * origin_list_or_null * _M.OWS
-
--- Analogue to RFC 7230 Section 7's ABNF extension of '#'
--- Also documented as `#rule` under RFC 2616 Section 2.1
-local comma_sep, comma_sep_trim do
-	local sep = _M.OWS * lpeg.P "," * _M.OWS
-	local optional_sep = (lpeg.P"," + core.SP + core.HTAB)^0
-	comma_sep = function(element, min, max)
-		local extra = sep * optional_sep * element
-		local patt = element
-		if min then
-			for _=2, min do
-				patt = patt * extra
-			end
-		else
-			min = 0
-			patt = patt^-1
-		end
-		if max then
-			local more = max-min-1
-			patt = patt * extra^-more
-		else
-			patt = patt * extra^0
-		end
-		return patt
-	end
-	-- allows leading + trailing
-	comma_sep_trim = function (...)
-		return optional_sep * comma_sep(...) * optional_sep
-	end
-end
-
--- RFC 7034
-_M.X_Frame_Options = case_insensitive "deny" * Cc("deny")
-	+ case_insensitive "sameorigin" * Cc("sameorigin")
-	+ case_insensitive "allow-from" * _M.RWS * serialized_origin
-
--- RFC 7230 Section 2.6
-local HTTP_name = P"HTTP"
-local HTTP_version = HTTP_name * P"/" * (core.DIGIT * P"." * core.DIGIT / util.safe_tonumber)
-
--- RFC 7230 Section 2.7
-local absolute_path = (P"/" * uri.segment )^1
-local partial_uri = Ct(uri.relative_part * (P"?" * uri.query)^-1)
-
--- RFC 7230 Section 3.2.6
-local tchar = S "!#$%&'*+-.^_`|~" + core.DIGIT + core.ALPHA
-_M.token = C(tchar^1)
-local obs_text = R("\128\255")
-_M.qdtext = core.HTAB + core.SP + P"\33" + R("\35\91", "\93\126") + obs_text
-local quoted_pair = Cs(P"\\" * C(core.HTAB + core.SP + core.VCHAR + obs_text) / "%1")
-_M.quoted_string = core.DQUOTE * Cs((_M.qdtext + quoted_pair)^0) * core.DQUOTE
-
-local ctext = core.HTAB + core.SP + R("\33\39", "\42\91", "\93\126") + obs_text
-_M.comment = P { P"(" * ( ctext + quoted_pair + V(1) )^0 * P")" }
-
--- RFC 7230 Section 3.2
-_M.field_name = _M.token / string.lower -- case insensitive
-local field_vchar = core.VCHAR + obs_text
-local field_content = field_vchar * (( core.SP + core.HTAB )^1 * field_vchar)^-1
-local obs_fold = ( core.SP + core.HTAB )^0 * core.CRLF * ( core.SP + core.HTAB )^1 / " "
--- field_value is not correct, see Errata: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
-_M.field_value = Cs((field_content + obs_fold)^0)
-_M.header_field = _M.field_name * P":" * _M.OWS * _M.field_value * _M.OWS
-
--- RFC 7230 Section 3.3.2
-_M.Content_Length = core.DIGIT^1
-
--- RFC 7230 Section 4
--- See https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4683
-local transfer_parameter = (_M.token - S"qQ" * _M.BWS * P"=") * _M.BWS * P"=" * _M.BWS * ( _M.token + _M.quoted_string )
-local transfer_extension = Cf(Ct(_M.token / string.lower) -- case insensitive
-	* ( _M.OWS * P";" * _M.OWS * Cg(transfer_parameter) )^0, rawset)
-local transfer_coding = transfer_extension
-
--- RFC 7230 Section 3.3.1
-_M.Transfer_Encoding = comma_sep_trim(transfer_coding, 1)
-
--- RFC 7230 Section 4.1.1
-local chunk_ext_name = _M.token
-local chunk_ext_val = _M.token + _M.quoted_string
--- See https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4667
-_M.chunk_ext = ( P";" * chunk_ext_name * ( P"=" * chunk_ext_val)^-1 )^0
-
--- RFC 7230 Section 4.3
-local rank = (P"0" * ((P"." * core.DIGIT^-3) / util.safe_tonumber + Cc(0)) + P"1" * ("." * (P"0")^-3)^-1) * Cc(1)
-local t_ranking = _M.OWS * P";" * _M.OWS * S"qQ" * P"=" * rank -- q is case insensitive
-local t_codings = (transfer_coding * t_ranking^-1) / function(t, q)
-	if q then
-		t["q"] = q
-	end
-	return t
-end
-_M.TE = comma_sep_trim(t_codings)
-
--- RFC 7230 Section 4.4
-_M.Trailer = comma_sep_trim(_M.field_name, 1)
-
--- RFC 7230 Section 5.3
-local origin_form = Cs(absolute_path * (P"?" * uri.query)^-1)
-local absolute_form = no_rich_capture(uri.absolute_uri)
-local authority_form = no_rich_capture(uri.authority)
-local asterisk_form = C"*"
-_M.request_target = asterisk_form + origin_form + absolute_form + authority_form
-
--- RFC 7230 Section 3.1.1
-local method = _M.token
-_M.request_line = method * core.SP * _M.request_target * core.SP * HTTP_version * core.CRLF
-
--- RFC 7230 Section 5.4
-_M.Host = uri.host * (P":" * uri.port)^-1
-
--- RFC 7230 Section 6.7
-local protocol_name = _M.token
-local protocol_version = _M.token
-local protocol = protocol_name * (P"/" * protocol_version)^-1 / "%0"
-_M.Upgrade = comma_sep_trim(protocol)
-
--- RFC 7230 Section 5.7.1
-local received_protocol = (protocol_name * P"/" + Cc("HTTP")) * protocol_version / "%1/%2"
-local pseudonym = _M.token
--- workaround for https://lists.w3.org/Archives/Public/ietf-http-wg/2016OctDec/0527.html
-local received_by = uri.host * ((P":" * uri.port) + -lpeg.B(",")) / "%0" + pseudonym
-_M.Via = comma_sep_trim(Ct(Cg(received_protocol, "protocol") * _M.RWS * Cg(received_by, "by") * (_M.RWS * Cg(_M.comment, "comment"))^-1), 1)
-
--- RFC 7230 Section 6.1
-local connection_option = _M.token / string.lower -- case insensitive
-_M.Connection = comma_sep_trim(connection_option)
-
--- RFC 7231 Section 3.1.1
-local content_coding = _M.token / string.lower -- case insensitive
-_M.Content_Encoding = comma_sep_trim(content_coding, 1)
-
--- RFC 7231 Section 3.1.2
-local type = _M.token / string.lower -- case insensitive
-local subtype = _M.token / string.lower -- case insensitive
-local parameter = _M.token / string.lower -- case insensitive
-	* P"=" * (_M.token + _M.quoted_string)
-local media_type = Cg(type, "type") * P"/" * Cg(subtype, "subtype")
-	* Cg(Cf(Ct(true) * (_M.OWS * P";" * _M.OWS * Cg(parameter))^0, rawset), "parameters")
-local charset = _M.token / string.lower -- case insensitive
-_M.Content_Type = Ct(media_type)
-
--- RFC 7231 Section 3.1.3
-_M.Content_Language = comma_sep_trim(language.Language_Tag, 1)
-
--- RFC 7231 Section 3.1.4.2
-_M.Content_Location = uri.absolute_uri + partial_uri
-
--- RFC 7231 Section 5.1.1
-_M.Expect = P"100-"*S"cC"*S"oO"*S"nN"*S"tT"*S"iI"*S"nN"*S"uU"*S"eE" * Cc("100-continue")
-
--- RFC 7231 Section 5.1.2
-_M.Max_Forwards = core.DIGIT^1 / tonumber
-
--- RFC 7231 Section 5.3.1
-local qvalue = rank -- luacheck: ignore 211
-local weight = t_ranking
-
--- RFC 7231 Section 5.3.2
-local media_range = (P"*/*"
-	+ (Cg(type, "type") * P"/*")
-	+ (Cg(type, "type") * P"/" * Cg(subtype, "subtype"))
-) * Cg(Cf(Ct(true) * (_M.OWS * ";" * _M.OWS * Cg(parameter) - weight)^0, rawset), "parameters")
-local accept_ext = _M.OWS * P";" * _M.OWS * _M.token * (P"=" * (_M.token + _M.quoted_string))^-1
-local accept_params = Cg(weight, "q") * Cg(Cf(Ct(true) * Cg(accept_ext)^0, rawset), "extensions")
-_M.Accept = comma_sep_trim(Ct(media_range * (accept_params+Cg(Ct(true), "extensions"))))
-
--- RFC 7231 Section 5.3.3
-_M.Accept_Charset = comma_sep_trim((charset + P"*") * weight^-1, 1)
-
--- RFC 7231 Section 5.3.4
-local codings = content_coding + "*"
-_M.Accept_Encoding = comma_sep_trim(codings * weight^-1)
-
--- RFC 4647 Section 2.1
-local alphanum = core.ALPHA + core.DIGIT
-local language_range = (core.ALPHA * core.ALPHA^-7 * (P"-" * alphanum * alphanum^-7)^0) + P"*"
--- RFC 7231 Section 5.3.5
-_M.Accept_Language = comma_sep_trim(language_range * weight^-1, 1)
-
--- RFC 7231 Section 5.5.1
-_M.From = email.mailbox
-
--- RFC 7231 Section 5.5.2
-_M.Referer = uri.absolute_uri + partial_uri
-
--- RFC 7231 Section 5.5.3
-local product_version = _M.token
-local product = _M.token * (P"/" * product_version)^-1
-_M.User_Agent = product * (_M.RWS * (product + _M.comment))^0
-
--- RFC 7231 Section 7.1.1.1
--- Uses os.date field names
-local day_name = Cg(P"Mon"*Cc(2)
-	+ P"Tue"*Cc(3)
-	+ P"Wed"*Cc(4)
-	+ P"Thu"*Cc(5)
-	+ P"Fri"*Cc(6)
-	+ P"Sat"*Cc(7)
-	+ P"Sun"*Cc(1), "wday")
-local day = Cg(core.DIGIT * core.DIGIT / tonumber, "day")
-local month = Cg(P"Jan"*Cc(1)
-	+ P"Feb"*Cc(2)
-	+ P"Mar"*Cc(3)
-	+ P"Apr"*Cc(4)
-	+ P"May"*Cc(5)
-	+ P"Jun"*Cc(6)
-	+ P"Jul"*Cc(7)
-	+ P"Aug"*Cc(8)
-	+ P"Sep"*Cc(9)
-	+ P"Oct"*Cc(10)
-	+ P"Nov"*Cc(11)
-	+ P"Dec"*Cc(12), "month")
-local year = Cg(core.DIGIT * core.DIGIT * core.DIGIT * core.DIGIT / tonumber, "year")
-local date1 = day * core.SP * month * core.SP * year
-
-local GMT = P"GMT"
-
-local minute = Cg(core.DIGIT * core.DIGIT / tonumber, "min")
-local second = Cg(core.DIGIT * core.DIGIT / tonumber, "sec")
-local hour = Cg(core.DIGIT * core.DIGIT / tonumber, "hour")
--- XXX only match 00:00:00 - 23:59:60 (leap second)?
-
-local time_of_day = hour * P":" * minute * P":" * second
-_M.IMF_fixdate = Ct(day_name * P"," * core.SP * date1 * core.SP * time_of_day * core.SP * GMT)
-
-local date2 do
-	local year_barrier = 70
-	local twodayyear = Cg(core.DIGIT * core.DIGIT / function(y)
-		y = tonumber(y, 10)
-		if y < year_barrier then
-			return 2000+y
-		else
-			return 1900+y
-		end
-	end, "year")
-	date2 = day * P"-" * month * P"-" * twodayyear
-end
-local day_name_l = Cg(P"Monday"*Cc(2)
-	+ P"Tuesday"*Cc(3)
-	+ P"Wednesday"*Cc(4)
-	+ P"Thursday"*Cc(5)
-	+ P"Friday"*Cc(6)
-	+ P"Saturday"*Cc(7)
-	+ P"Sunday"*Cc(1), "wday")
-local rfc850_date = Ct(day_name_l * P"," * core.SP * date2 * core.SP * time_of_day * core.SP * GMT)
-
-local date3 = month * core.SP * (day + Cg(core.SP * core.DIGIT / tonumber, "day"))
-local asctime_date = Ct(day_name * core.SP * date3 * core.SP * time_of_day * core.SP * year)
-local obs_date = rfc850_date + asctime_date
-
-local HTTP_date = _M.IMF_fixdate + obs_date
-_M.Date = HTTP_date
-
--- RFC 7231 Section 7.1.2
-_M.Location = uri.uri_reference
-
--- RFC 7231 Section 7.1.3
-local delay_seconds = core.DIGIT^1 / tonumber
-_M.Retry_After = HTTP_date + delay_seconds
-
--- RFC 7231 Section 7.1.4
-_M.Vary = P"*" + comma_sep(_M.field_name, 1)
-
--- RFC 7231 Section 7.4.1
-_M.Allow = comma_sep_trim(method)
-
--- RFC 7231 Section 7.4.2
-_M.Server = product * (_M.RWS * (product + _M.comment))^0
+_M.SLUG = require "lpeg_patterns.http.slug".SLUG
 
 -- RFC 5789
-_M.Accept_Patch = comma_sep_trim(media_type, 1)
-
--- RFC 5987
-local attr_char = core.ALPHA + core.DIGIT + S"!#$&+-.^_`|~"
--- can't use uri.pct_encoded, as it doesn't decode all characters
-local pct_encoded = P"%" * (core.HEXDIG * core.HEXDIG / util.read_hex) / string.char
-local value_chars = Cs((pct_encoded + attr_char)^0)
-local parmname = C(attr_char^1)
-local ext_value do
-	-- ext-value uses charset from RFC 5987 instead
-	local mime_charsetc = core.ALPHA + core.DIGIT + S"!#$%&+-^_`{}~"
-	local mime_charset = C(mime_charsetc^1)
-	ext_value = Cg(mime_charset, "charset") * P"'" * Cg(language.Language_Tag, "language")^-1 * P"'" * value_chars
-end
+_M.Accept_Patch = http_core.comma_sep_trim(http_semantics.media_type, 1)
 
-do -- RFC 5988
-	local ptokenchar = S"!#$%&'()*+-./:<=>?@[]^_`{|}~" + core.DIGIT + core.ALPHA
-	local ptoken = ptokenchar^1
-	local ext_name_star = parmname * P"*"
-	local link_extension = ext_name_star * P"=" * ext_value
-		+ parmname * (P"=" * (ptoken + _M.quoted_string))^-1
-	-- See https://www.rfc-editor.org/errata_search.php?rfc=5988&eid=3158
-	local link_param = link_extension
-	local link_value = Cf(Ct(P"<" * uri.uri_reference * P">") * (_M.OWS * P";" * _M.OWS * Cg(link_param))^0, rawset)
-	-- TODO: handle multiple ext_value variants...
-	-- e.g. server might provide one title in english, one in chinese, client should be able to pick which one to display
-	_M.Link = comma_sep_trim(link_value)
-end
+-- RFC 5988
+_M.Link = require "lpeg_patterns.http.link".Link
 
-do -- RFC 6265
-	local cookie_name = _M.token
-	local cookie_octet = S"!" + R("\35\43", "\45\58", "\60\91", "\93\126")
-	local cookie_value = core.DQUOTE * C(cookie_octet^0) * core.DQUOTE + C(cookie_octet^0)
-	local cookie_pair = cookie_name * _M.BWS * P"=" * _M.BWS * cookie_value * _M.BWS
+-- RFC 6265
+local http_cookie = require "lpeg_patterns.http.cookie"
+_M.Cookie = http_cookie.Cookie
+_M.Set_Cookie = http_cookie.Set_Cookie
 
-	local ext_char = core.CHAR - core.CTL - S";"
-	ext_char = ext_char - core.WSP + core.WSP * #(core.WSP^0 * ext_char) -- No trailing whitespace
-	-- Complexity is to make sure whitespace before an `=` isn't captured
-	local extension_av = ((ext_char - S"=" - core.WSP) + core.WSP^1 * #(1-S"="))^0 / string.lower
-			* _M.BWS * P"=" * _M.BWS * C(ext_char^0)
-		+ (ext_char)^0 / string.lower * Cc(true)
-	local cookie_av = extension_av
-	local set_cookie_string = cookie_pair * Cf(Ct(true) * (P";" * _M.OWS * Cg(cookie_av))^0, rawset)
-	_M.Set_Cookie = set_cookie_string
+-- RFC 6266
+_M.Content_Disposition = require "lpeg_patterns.http.disposition".Content_Disposition
 
-	local cookie_string = Cf(Ct(true) * Cg(cookie_pair) * (P";" * _M.OWS * Cg(cookie_pair))^0, rawset)
-	_M.Cookie = cookie_string
-end
-
-do -- RFC 6266
-	local disp_ext_type = _M.token / string.lower
-	local disposition_type = disp_ext_type
-	local ext_token = C((tchar-P"*"*(-tchar))^1) * P"*" -- can't use 'token' here as we need to not include the "*" at the end
-	local value = _M.token + _M.quoted_string
-	local disp_ext_parm = ext_token * _M.OWS * P"=" * _M.OWS * ext_value
-		+ _M.token * _M.OWS * P"=" * _M.OWS * value
-	local disposition_parm = disp_ext_parm
-	_M.Content_Disposition = disposition_type * Cf(Ct(true) * (_M.OWS * P";" * _M.OWS * Cg(disposition_parm))^0, rawset)
-end
+-- RFC 6454
+_M.Origin = require "lpeg_patterns.http.origin".Origin
 
 -- RFC 6455
-local base64_character = core.ALPHA + core.DIGIT + S"+/"
-local base64_data = base64_character * base64_character * base64_character * base64_character
-local base64_padding = base64_character * base64_character * P"=="
-	+ base64_character * base64_character * base64_character * P"="
-local base64_value_non_empty = (base64_data^1 * base64_padding^-1) + base64_padding
-_M.Sec_WebSocket_Accept = base64_value_non_empty
-_M.Sec_WebSocket_Key = base64_value_non_empty
-local registered_token = _M.token
-local extension_token = registered_token
-local extension_param do
-	local EOF = P(-1)
-	local token_then_EOF = Cc(true) * _M.token * EOF
-	-- the quoted-string must be a valid token
-	local quoted_token = Cmt(_M.quoted_string, function(_, _, q)
-		return token_then_EOF:match(q)
-	end)
-	extension_param = _M.token * ((P"=" * (_M.token + quoted_token)) + Cc(true))
-end
-local extension = extension_token * Cg(Cf(Ct(true) * (P";" * Cg(extension_param))^0, rawset), "parameters")
-local extension_list = comma_sep_trim(Ct(extension))
-_M.Sec_WebSocket_Extensions = extension_list
-_M.Sec_WebSocket_Protocol_Client = comma_sep_trim(_M.token)
-_M.Sec_WebSocket_Protocol_Server = _M.token
-local NZDIGIT =  S"123456789"
--- Limited to 0-255 range, with no leading zeros
-local version = (
-	P"2" * (S"01234" * core.DIGIT + P"5" * S"012345")
-	+ (P"1") * core.DIGIT * core.DIGIT
-	+ NZDIGIT * core.DIGIT^-1
-) / tonumber
-_M.Sec_WebSocket_Version_Client = version
-_M.Sec_WebSocket_Version_Server = comma_sep_trim(version)
+local http_websocket = require "lpeg_patterns.http.websocket"
+_M.Sec_WebSocket_Accept = http_websocket.Sec_WebSocket_Accept
+_M.Sec_WebSocket_Key = http_websocket.Sec_WebSocket_Key
+_M.Sec_WebSocket_Extensions = http_websocket.Sec_WebSocket_Extensions
+_M.Sec_WebSocket_Protocol_Client = http_websocket.Sec_WebSocket_Protocol_Client
+_M.Sec_WebSocket_Protocol_Server = http_websocket.Sec_WebSocket_Protocol_Server
+_M.Sec_WebSocket_Version_Client = http_websocket.Sec_WebSocket_Version_Client
+_M.Sec_WebSocket_Version_Server = http_websocket.Sec_WebSocket_Version_Server
 
 -- RFC 6797
-local directive_name = _M.token / string.lower
-local directive_value = _M.token + _M.quoted_string
-local directive = Cg(directive_name * ((_M.OWS * P"=" * _M.OWS * directive_value) + Cc(true)))
-_M.Strict_Transport_Security = directive^-1 * (_M.OWS * P";" * _M.OWS * directive^-1)^0
-
--- RFC 7089
-_M.Accept_Datetime = _M.IMF_fixdate
-_M.Memento_Datetime = _M.IMF_fixdate
-
--- RFC 7232 Section 2.2
-_M.Last_Modified = HTTP_date
-
--- RFC 7232 Section 2.3
-local weak = P"W/" -- case sensitive
-local etagc = P"\33" + R"\35\115" + obs_text
-local opaque_tag = core.DQUOTE * etagc^0 * core.DQUOTE
-local entity_tag = Cg(weak*Cc(true) + Cc(false), "weak") * C(opaque_tag)
-_M.ETag = entity_tag
-
--- RFC 7232 Section 3.1
-_M.If_Match = P"*" + comma_sep(entity_tag, 1)
-
--- RFC 7232 Section 3.2
-_M.If_None_Match = P"*" + comma_sep(entity_tag, 1)
-
--- RFC 7232 Section 3.3
-_M.If_Modified_Since = HTTP_date
-
--- RFC 7232 Section 3.4
-_M.If_Unmodified_Since = HTTP_date
+_M.Strict_Transport_Security = require "lpeg_patterns.http.sts".Strict_Transport_Security
 
--- RFC 4918
-local Coded_URL = P"<" * uri.absolute_uri * P">"
-local extend = Coded_URL + _M.token
-local compliance_class = P"1" + P"2" + P"3" + extend
-_M.DAV = comma_sep_trim(compliance_class)
-_M.Depth = P"0" * Cc(0)
-	+ P"1" * Cc(1)
-	+ case_insensitive "infinity" * Cc(math.huge)
-local Simple_ref = uri.absolute_uri + partial_uri
-_M.Destination = Simple_ref
-local State_token = Coded_URL
-local Condition = (case_insensitive("not") * Cc("not"))^-1
-	* _M.OWS * (State_token + P"[" * entity_tag * P"]")
-local List = P"(" * _M.OWS * (Condition * _M.OWS)^1 * P")"
-local No_tag_list = List
-local Resource_Tag = P"<" * Simple_ref * P">"
-local Tagged_list = Resource_Tag * _M.OWS * (List * _M.OWS)^1
-_M.If = (Tagged_list * _M.OWS)^1 + (No_tag_list * _M.OWS)^1
-_M.Lock_Token = Coded_URL
-_M.Overwrite = T_F
-local DAVTimeOutVal = core.DIGIT^1 / tonumber
-local TimeType = case_insensitive "Second-" * DAVTimeOutVal
-	+ case_insensitive "Infinite" * Cc(math.huge)
-_M.TimeOut = comma_sep_trim(TimeType)
-
--- RFC 5323
-_M.DASL = comma_sep_trim(Coded_URL, 1)
-
--- RFC 6638
-_M.Schedule_Reply = T_F
-_M.Schedule_Tag = opaque_tag
-_M.If_Schedule_Tag_Match = opaque_tag
-
--- RFC 7233
-local bytes_unit = P"bytes"
-local other_range_unit = _M.token
-local range_unit = C(bytes_unit) + other_range_unit
-
-local first_byte_pos = core.DIGIT^1 / tonumber
-local last_byte_pos = core.DIGIT^1 / tonumber
-local byte_range_spec = first_byte_pos * P"-" * last_byte_pos^-1
-local suffix_length = core.DIGIT^1 / tonumber
-local suffix_byte_range_spec = Cc(nil) * P"-" * suffix_length
-local byte_range_set = comma_sep(byte_range_spec + suffix_byte_range_spec, 1)
-local byte_ranges_specifier = bytes_unit * P"=" * byte_range_set
-
--- RFC 7233 Section 2.3
-local acceptable_ranges = comma_sep_trim(range_unit, 1) + P"none"
-_M.Accept_Ranges = acceptable_ranges
-
--- RFC 7233 Section 3.1
-local other_range_set = core.VCHAR^1
-local other_ranges_specifier = other_range_unit * P"=" * other_range_set
-_M.Range = byte_ranges_specifier + other_ranges_specifier
-
--- RFC 7233 Section 3.2
-_M.If_Range = entity_tag + HTTP_date
-
--- RFC 7233 Section 4.2
-local complete_length = core.DIGIT^1 / tonumber
-local unsatisfied_range = P"*/" * complete_length
-local byte_range = first_byte_pos * P"-" * last_byte_pos
-local byte_range_resp = byte_range * P"/" * (complete_length + P"*")
-local byte_content_range = bytes_unit * core.SP * (byte_range_resp + unsatisfied_range)
-local other_range_resp = core.CHAR^0
-local other_content_range = other_range_unit * core.SP * other_range_resp
-_M.Content_Range = byte_content_range + other_content_range
-
--- RFC 7234 Section 1.2.1
-local delta_seconds = core.DIGIT^1 / tonumber
-
--- RFC 7234 Section 5.1
-_M.Age = delta_seconds
-
--- RFC 7234 Section 5.2
-local cache_directive = _M.token * (P"=" * (_M.token + _M.quoted_string))^-1
-_M.Cache_Control = comma_sep_trim(cache_directive, 1)
-
--- RFC 7234 Section 5.3
-_M.Expires = HTTP_date
-
--- RFC 7234 Section 5.4
-local extension_pragma = _M.token * (P"=" * (_M.token + _M.quoted_string))^-1
-local pragma_directive = "no_cache" + extension_pragma
-_M.Pragma = comma_sep_trim(pragma_directive, 1)
-
--- RFC 7234 Section 5.5
-local warn_code = core.DIGIT * core.DIGIT * core.DIGIT
-local warn_agent = (uri.host * (P":" * uri.port)^-1) + pseudonym
-local warn_text = _M.quoted_string
-local warn_date = core.DQUOTE * HTTP_date * core.DQUOTE
-local warning_value = warn_code * core.SP * warn_agent * core.SP * warn_text * (core.SP * warn_date)^-1
-_M.Warning = comma_sep_trim(warning_value, 1)
-
--- RFC 7235 Section 2
-local auth_scheme = _M.token
-local auth_param = Cg(_M.token / string.lower * _M.BWS * P"=" * _M.BWS * (_M.token + _M.quoted_string))
-local token68 = C((core.ALPHA + core.DIGIT + P"-" + P"." + P"_" + P"~" + P"+" + P"/" )^1 * (P"=")^0)
--- TODO: each parameter name MUST only occur once per challenge
-local challenge = auth_scheme * (core.SP^1 * (Cf(Ct(true) * comma_sep(auth_param), rawset) + token68))^-1
-local credentials = challenge
+-- RFC 7034
+_M.X_Frame_Options = require "lpeg_patterns.http.frameoptions".X_Frame_Options
 
--- RFC 7235 Section 4
-_M.WWW_Authenticate = comma_sep_trim(Ct(challenge), 1)
-_M.Authorization = credentials
-_M.Proxy_Authenticate = _M.WWW_Authenticate
-_M.Proxy_Authorization = _M.Proxy_Authorization
+-- RFC 7089
+_M.Accept_Datetime = http_semantics.IMF_fixdate
+_M.Memento_Datetime = http_semantics.IMF_fixdate
 
--- RFC 7239 Section 4
-local value = _M.token + _M.quoted_string
-local forwarded_pair = _M.token * P"=" * value
-local forwarded_element = forwarded_pair^-1 * (P";" * forwarded_pair^-1)^0
-_M.Forwarded = comma_sep_trim(forwarded_element)
+-- RFC 7239
+_M.Forwarded = require "lpeg_patterns.http.forwarded".Forwarded
 
 -- RFC 7469
-local Public_Key_Directives = directive * (_M.OWS * P";" * _M.OWS * directive)^0
-_M.Public_Key_Pins = Public_Key_Directives
-_M.Public_Key_Pins_Report_Only = Public_Key_Directives
+local http_pkp = require "lpeg_patterns.http.pkp"
+_M.Public_Key_Pins = http_pkp.Public_Key_Pins
+_M.Public_Key_Pins_Report_Only = http_pkp.Public_Key_Pins_Report_Only
 
 -- RFC 7486
-_M.Hobareg = C"regok" + C"reginwork"
+_M.Hobareg = require "lpeg_patterns.http.hoba".Hobareg
 
 -- RFC 7615
-_M.Authentication_Info = comma_sep_trim(auth_param)
-_M.Proxy_Authentication_Info = comma_sep_trim(auth_param)
+_M.Authentication_Info = http_authentication.Authentication_Info
+_M.Proxy_Authentication_Info = http_authentication.Proxy_Authentication_Info
 
 -- RFC 7639
-local protocol_id = _M.token
-_M.ALPN = comma_sep_trim(protocol_id, 1)
-
--- RFC 7809
-_M.CalDAV_Timezones = T_F
+_M.ALPN = require "lpeg_patterns.http.alpn".ALPN
 
 -- RFC 7838
-local clear = C"clear" -- case-sensitive
-local alt_authority = _M.quoted_string -- containing [ uri_host ] ":" port
-local alternative = protocol_id * P"=" * alt_authority
-local alt_value = alternative * (_M.OWS * P";" * _M.OWS * parameter)^0
-_M.Alt_Svc = clear + comma_sep_trim(alt_value, 1)
-_M.Alt_Used = uri.host * (P":" * uri.port)^-1
+local http_alternate = require "lpeg_patterns.http.alternate"
+_M.Alt_Svc = http_alternate.Alt_Svc
+_M.Alt_Used = http_alternate.Alt_Used
+
+-- https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-06#section-2.1
+_M.Expect_CT = require "lpeg_patterns.http.expect_ct".Expect_CT
+
+-- https://www.w3.org/TR/referrer-policy/#referrer-policy-header
+_M.Referrer_Policy = require "lpeg_patterns.http.referrer_policy".Referrer_Policy
 
 return _M
diff --git a/lpeg_patterns/http/alpn.lua b/lpeg_patterns/http/alpn.lua
new file mode 100644
index 0000000..d111fe4
--- /dev/null
+++ b/lpeg_patterns/http/alpn.lua
@@ -0,0 +1,36 @@
+-- RFC 7639
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+local util = require "lpeg_patterns.util"
+
+local Cmt = lpeg.Cmt
+local Cs = lpeg.Cs
+local P = lpeg.P
+local R = lpeg.R
+
+--[[ protocol-id is a percent-encoded ALPN protocol name
+  - Octets in the ALPN protocol MUST NOT be percent-encoded if they
+	are valid token characters except "%".
+  - When using percent-encoding, uppercase hex digits MUST be used.
+]]
+
+local valid_chars = http_core.tchar - P"%"
+local upper_hex = R("09", "AF")
+local percent_char = P"%" * (upper_hex * upper_hex / util.read_hex) / string.char
+local percent_encoded = Cmt(percent_char, function(_, _, c)
+	-- check that decoded character would not have been allowed unescaped
+	if not valid_chars:match(c) then
+		return true, c
+	end
+end)
+local percent_replace = Cs((valid_chars + percent_encoded)^0)
+
+local protocol_id = percent_replace
+
+local ALPN = http_core.comma_sep_trim(protocol_id, 1)
+
+return {
+	protocol_id = protocol_id;
+	ALPN = ALPN;
+}
diff --git a/lpeg_patterns/http/alternate.lua b/lpeg_patterns/http/alternate.lua
new file mode 100644
index 0000000..7546e08
--- /dev/null
+++ b/lpeg_patterns/http/alternate.lua
@@ -0,0 +1,23 @@
+-- RFC 7838
+-- HTTP Alternative Services
+
+local lpeg = require "lpeg"
+local http_alpn = require "lpeg_patterns.http.alpn"
+local http_core = require "lpeg_patterns.http.core"
+local http_semantics = require "lpeg_patterns.http.semantics"
+local uri = require "lpeg_patterns.uri"
+
+local C = lpeg.C
+local P = lpeg.P
+
+local clear = C"clear" -- case-sensitive
+local alt_authority = http_core.quoted_string -- containing [ uri_host ] ":" port
+local alternative = http_alpn.protocol_id * P"=" * alt_authority
+local alt_value = alternative * (http_core.OWS * P";" * http_core.OWS * http_semantics.parameter)^0
+local Alt_Svc = clear + http_core.comma_sep_trim(alt_value, 1)
+local Alt_Used = uri.host * (P":" * uri.port)^-1
+
+return {
+	Alt_Svc = Alt_Svc;
+	Alt_Used = Alt_Used;
+}
diff --git a/lpeg_patterns/http/authentication.lua b/lpeg_patterns/http/authentication.lua
new file mode 100644
index 0000000..c8bffe6
--- /dev/null
+++ b/lpeg_patterns/http/authentication.lua
@@ -0,0 +1,36 @@
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+
+local C = lpeg.C
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local P = lpeg.P
+
+-- RFC 7235 Section 2
+local auth_scheme = http_core.token
+local auth_param = Cg(http_core.token / string.lower * http_core.BWS * P"=" * http_core.BWS * (http_core.token + http_core.quoted_string))
+local token68 = C((core.ALPHA + core.DIGIT + P"-" + P"." + P"_" + P"~" + P"+" + P"/" )^1 * (P"=")^0)
+-- TODO: each parameter name MUST only occur once per challenge
+local challenge = auth_scheme * (core.SP^1 * (Cf(Ct(true) * http_core.comma_sep(auth_param), rawset) + token68))^-1
+local credentials = challenge
+
+-- RFC 7235 Section 4
+local WWW_Authenticate = http_core.comma_sep_trim(Ct(challenge), 1)
+local Authorization = credentials
+local Proxy_Authenticate = WWW_Authenticate
+local Proxy_Authorization = Authorization
+
+-- RFC 7615
+local Authentication_Info = http_core.comma_sep_trim(auth_param)
+local Proxy_Authentication_Info = http_core.comma_sep_trim(auth_param)
+
+return {
+	Authentication_Info = Authentication_Info;
+	Authorization = Authorization;
+	Proxy_Authenticate = Proxy_Authenticate;
+	Proxy_Authentication_Info = Proxy_Authentication_Info;
+	Proxy_Authorization = Proxy_Authorization;
+	WWW_Authenticate = WWW_Authenticate;
+}
diff --git a/lpeg_patterns/http/caching.lua b/lpeg_patterns/http/caching.lua
new file mode 100644
index 0000000..fc3d020
--- /dev/null
+++ b/lpeg_patterns/http/caching.lua
@@ -0,0 +1,46 @@
+-- RFC 7234
+-- Hypertext Transfer Protocol (HTTP/1.1): Caching
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+local http_semantics = require "lpeg_patterns.http.semantics"
+local uri = require "lpeg_patterns.uri"
+
+local Cc = lpeg.Cc
+local Cg = lpeg.Cg
+local P = lpeg.P
+
+-- RFC 7234 Section 1.2.1
+local delta_seconds = core.DIGIT^1 / tonumber
+
+-- RFC 7234 Section 5.1
+local Age = delta_seconds
+
+-- RFC 7234 Section 5.2
+local cache_directive = http_core.token / string.lower * ((P"=" * (http_core.token + http_core.quoted_string)) + Cc(true))
+local Cache_Control = http_core.comma_sep_trim(Cg(cache_directive), 1)
+
+-- RFC 7234 Section 5.3
+local Expires = http_semantics.HTTP_date
+
+-- RFC 7234 Section 5.4
+local extension_pragma = http_core.token * (P"=" * (http_core.token + http_core.quoted_string))^-1
+local pragma_directive = "no_cache" + extension_pragma
+local Pragma = http_core.comma_sep_trim(pragma_directive, 1)
+
+-- RFC 7234 Section 5.5
+local warn_code = core.DIGIT * core.DIGIT * core.DIGIT
+local warn_agent = (uri.host * (P":" * uri.port)^-1) + http_core.pseudonym
+local warn_text = http_core.quoted_string
+local warn_date = core.DQUOTE * http_semantics.HTTP_date * core.DQUOTE
+local warning_value = warn_code * core.SP * warn_agent * core.SP * warn_text * (core.SP * warn_date)^-1
+local Warning = http_core.comma_sep_trim(warning_value, 1)
+
+return {
+	Age = Age;
+	Cache_Control = Cache_Control;
+	Expires = Expires;
+	Pragma = Pragma;
+	Warning = Warning;
+}
diff --git a/lpeg_patterns/http/conditional.lua b/lpeg_patterns/http/conditional.lua
new file mode 100644
index 0000000..02bc430
--- /dev/null
+++ b/lpeg_patterns/http/conditional.lua
@@ -0,0 +1,47 @@
+-- RFC 7232
+-- Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+local http_semantics = require "lpeg_patterns.http.semantics"
+
+local C = lpeg.C
+local Cc = lpeg.Cc
+local Cg = lpeg.Cg
+local P = lpeg.P
+local R = lpeg.R
+
+-- RFC 7232 Section 2.2
+local Last_Modified = http_semantics.HTTP_date
+
+-- RFC 7232 Section 2.3
+local weak = P"W/" -- case sensitive
+local etagc = P"\33" + R"\35\115" + http_core.obs_text
+local opaque_tag = core.DQUOTE * etagc^0 * core.DQUOTE
+local entity_tag = Cg(weak*Cc(true) + Cc(false), "weak") * C(opaque_tag)
+local ETag = entity_tag
+
+-- RFC 7232 Section 3.1
+local If_Match = P"*" + http_core.comma_sep(entity_tag, 1)
+
+-- RFC 7232 Section 3.2
+local If_None_Match = P"*" + http_core.comma_sep(entity_tag, 1)
+
+-- RFC 7232 Section 3.3
+local If_Modified_Since = http_semantics.HTTP_date
+
+-- RFC 7232 Section 3.4
+local If_Unmodified_Since = http_semantics.HTTP_date
+
+return {
+	entity_tag = entity_tag;
+	opaque_tag = opaque_tag;
+
+	Last_Modified = Last_Modified;
+	ETag = ETag;
+	If_Match = If_Match;
+	If_None_Match = If_None_Match;
+	If_Modified_Since = If_Modified_Since;
+	If_Unmodified_Since = If_Unmodified_Since;
+}
diff --git a/lpeg_patterns/http/cookie.lua b/lpeg_patterns/http/cookie.lua
new file mode 100644
index 0000000..9056f5b
--- /dev/null
+++ b/lpeg_patterns/http/cookie.lua
@@ -0,0 +1,37 @@
+-- RFC 6265
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+
+local C = lpeg.C
+local Cc = lpeg.Cc
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local P = lpeg.P
+local R = lpeg.R
+local S = lpeg.S
+
+local cookie_name = http_core.token
+local cookie_octet = S"!" + R("\35\43", "\45\58", "\60\91", "\93\126")
+local cookie_value = core.DQUOTE * C(cookie_octet^0) * core.DQUOTE + C(cookie_octet^0)
+local cookie_pair = cookie_name * http_core.BWS * P"=" * http_core.BWS * cookie_value * http_core.BWS
+
+local ext_char = core.CHAR - core.CTL - S";"
+ext_char = ext_char - core.WSP + core.WSP * #(core.WSP^0 * ext_char) -- No trailing whitespace
+-- Complexity is to make sure whitespace before an `=` isn't captured
+local extension_av = ((ext_char - S"=" - core.WSP) + core.WSP^1 * #(1-S"="))^0 / string.lower
+		* http_core.BWS * P"=" * http_core.BWS * C(ext_char^0)
+	+ (ext_char)^0 / string.lower * Cc(true)
+local cookie_av = extension_av
+local set_cookie_string = cookie_pair * Cf(Ct(true) * (P";" * http_core.OWS * Cg(cookie_av))^0, rawset)
+local Set_Cookie = set_cookie_string
+
+local cookie_string = Cf(Ct(true) * Cg(cookie_pair) * (P";" * http_core.OWS * Cg(cookie_pair))^0, rawset)
+local Cookie = cookie_string
+
+return {
+	Cookie = Cookie;
+	Set_Cookie = Set_Cookie;
+}
diff --git a/lpeg_patterns/http/core.lua b/lpeg_patterns/http/core.lua
new file mode 100644
index 0000000..f89cb64
--- /dev/null
+++ b/lpeg_patterns/http/core.lua
@@ -0,0 +1,183 @@
+-- RFC 7230
+-- Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local uri = require "lpeg_patterns.uri"
+local util = require "lpeg_patterns.util"
+
+local C = lpeg.C
+local Cc = lpeg.Cc
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Cs = lpeg.Cs
+local Ct = lpeg.Ct
+local P = lpeg.P
+local R = lpeg.R
+local S = lpeg.S
+local V = lpeg.V
+
+-- RFC 7230 Section 3.2.3
+local OWS = (core.SP + core.HTAB)^0
+local RWS = (core.SP + core.HTAB)^1
+local BWS = OWS
+
+-- Analogue to RFC 7230 Section 7's ABNF extension of '#'
+-- Also documented as `#rule` under RFC 2616 Section 2.1
+local sep = OWS * lpeg.P "," * OWS
+local optional_sep = (lpeg.P"," + core.SP + core.HTAB)^0
+local function comma_sep(element, min, max)
+	local extra = sep * optional_sep * element
+	local patt = element
+	if min then
+		for _=2, min do
+			patt = patt * extra
+		end
+	else
+		min = 0
+		patt = patt^-1
+	end
+	if max then
+		local more = max-min-1
+		patt = patt * extra^-more
+	else
+		patt = patt * extra^0
+	end
+	return patt
+end
+-- allows leading + trailing
+local function comma_sep_trim(...)
+	return optional_sep * comma_sep(...) * optional_sep
+end
+
+-- RFC 7230 Section 2.6
+local HTTP_name = P"HTTP"
+local HTTP_version = HTTP_name * P"/" * (core.DIGIT * P"." * core.DIGIT / util.safe_tonumber)
+
+-- RFC 7230 Section 2.7
+local absolute_path = (P"/" * uri.segment )^1
+local partial_uri = Ct(uri.relative_part * (P"?" * uri.query)^-1)
+
+-- RFC 7230 Section 3.2.6
+local tchar = S "!#$%&'*+-.^_`|~" + core.DIGIT + core.ALPHA
+local token = C(tchar^1)
+local obs_text = R("\128\255")
+local qdtext = core.HTAB + core.SP + P"\33" + R("\35\91", "\93\126") + obs_text
+local quoted_pair = Cs(P"\\" * C(core.HTAB + core.SP + core.VCHAR + obs_text) / "%1")
+local quoted_string = core.DQUOTE * Cs((qdtext + quoted_pair)^0) * core.DQUOTE
+
+local ctext = core.HTAB + core.SP + R("\33\39", "\42\91", "\93\126") + obs_text
+local comment = P { P"(" * ( ctext + quoted_pair + V(1) )^0 * P")" }
+
+-- RFC 7230 Section 3.2
+local field_name = token / string.lower -- case insensitive
+local field_vchar = core.VCHAR + obs_text
+local field_content = field_vchar * (( core.SP + core.HTAB )^1 * field_vchar)^-1
+local obs_fold = ( core.SP + core.HTAB )^0 * core.CRLF * ( core.SP + core.HTAB )^1 / " "
+-- field_value is not correct, see Errata: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
+local field_value = Cs((field_content + obs_fold)^0)
+local header_field = field_name * P":" * OWS * field_value * OWS
+
+-- RFC 7230 Section 3.3.2
+local Content_Length = core.DIGIT^1
+
+-- RFC 7230 Section 4
+-- See https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4683
+local transfer_parameter = (token - S"qQ" * BWS * P"=") * BWS * P"=" * BWS * ( token + quoted_string )
+local transfer_extension = Cf(Ct(token / string.lower) -- case insensitive
+	* ( OWS * P";" * OWS * Cg(transfer_parameter) )^0, rawset)
+local transfer_coding = transfer_extension
+
+-- RFC 7230 Section 3.3.1
+local Transfer_Encoding = comma_sep_trim(transfer_coding, 1)
+
+-- RFC 7230 Section 4.1.1
+local chunk_ext_name = token
+local chunk_ext_val = token + quoted_string
+-- See https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4667
+local chunk_ext = ( P";" * chunk_ext_name * ( P"=" * chunk_ext_val)^-1 )^0
+
+-- RFC 7230 Section 4.3
+local rank = (P"0" * ((P"." * core.DIGIT^-3) / util.safe_tonumber + Cc(0)) + P"1" * ("." * (P"0")^-3)^-1) * Cc(1)
+local t_ranking = OWS * P";" * OWS * S"qQ" * P"=" * rank -- q is case insensitive
+local t_codings = (transfer_coding * t_ranking^-1) / function(t, q)
+	if q then
+		t["q"] = q
+	end
+	return t
+end
+local TE = comma_sep_trim(t_codings)
+
+-- RFC 7230 Section 4.4
+local Trailer = comma_sep_trim(field_name, 1)
+
+-- RFC 7230 Section 5.3
+local origin_form = Cs(absolute_path * (P"?" * uri.query)^-1)
+local absolute_form = util.no_rich_capture(uri.absolute_uri)
+local authority_form = util.no_rich_capture(uri.authority)
+local asterisk_form = C"*"
+local request_target = asterisk_form + origin_form + absolute_form + authority_form
+
+-- RFC 7230 Section 3.1.1
+local method = token
+local request_line = method * core.SP * request_target * core.SP * HTTP_version * core.CRLF
+
+-- RFC 7230 Section 5.4
+local Host = uri.host * (P":" * uri.port)^-1
+
+-- RFC 7230 Section 6.7
+local protocol_name = token
+local protocol_version = token
+local protocol = protocol_name * (P"/" * protocol_version)^-1 / "%0"
+local Upgrade = comma_sep_trim(protocol)
+
+-- RFC 7230 Section 5.7.1
+local received_protocol = (protocol_name * P"/" + Cc("HTTP")) * protocol_version / "%1/%2"
+local pseudonym = token
+-- workaround for https://lists.w3.org/Archives/Public/ietf-http-wg/2016OctDec/0527.html
+local received_by = uri.host * ((P":" * uri.port) + -lpeg.B(",")) / "%0" + pseudonym
+local Via = comma_sep_trim(Ct(
+	Cg(received_protocol, "protocol")
+	* RWS * Cg(received_by, "by")
+	* (RWS * Cg(comment, "comment"))^-1
+), 1)
+
+-- RFC 7230 Section 6.1
+local connection_option = token / string.lower -- case insensitive
+local Connection = comma_sep_trim(connection_option)
+
+return {
+	comma_sep = comma_sep;
+	comma_sep_trim = comma_sep_trim;
+
+	OWS = OWS;
+	RWS = RWS;
+	BWS = BWS;
+
+	chunk_ext = chunk_ext;
+	comment = comment;
+	field_name = field_name;
+	field_value = field_value;
+	header_field = header_field;
+	method = method;
+	obs_text = obs_text;
+	partial_uri = partial_uri;
+	pseudonym = pseudonym;
+	qdtext = qdtext;
+	quoted_string = quoted_string;
+	rank = rank;
+	request_line = request_line;
+	request_target = request_target;
+	t_ranking = t_ranking;
+	tchar = tchar;
+	token = token;
+
+	Connection = Connection;
+	Content_Length = Content_Length;
+	Host = Host;
+	TE = TE;
+	Trailer = Trailer;
+	Transfer_Encoding = Transfer_Encoding;
+	Upgrade = Upgrade;
+	Via = Via;
+}
diff --git a/lpeg_patterns/http/disposition.lua b/lpeg_patterns/http/disposition.lua
new file mode 100644
index 0000000..fa26d4e
--- /dev/null
+++ b/lpeg_patterns/http/disposition.lua
@@ -0,0 +1,27 @@
+-- RFC 6266
+-- Use of the Content-Disposition Header Field in the
+-- Hypertext Transfer Protocol (HTTP)
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+local http_parameters = require "lpeg_patterns.http.parameters"
+
+local C = lpeg.C
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local P = lpeg.P
+
+local disp_ext_type = http_core.token / string.lower
+local disposition_type = disp_ext_type
+-- can't use 'token' here as we need to not include the "*" at the end
+local ext_token = C((http_core.tchar-P"*"*(-http_core.tchar))^1) * P"*"
+local value = http_core.token + http_core.quoted_string
+local disp_ext_parm = ext_token * http_core.OWS * P"=" * http_core.OWS * http_parameters.ext_value
+	+ http_core.token * http_core.OWS * P"=" * http_core.OWS * value
+local disposition_parm = disp_ext_parm
+local Content_Disposition = disposition_type * Cf(Ct(true) * (http_core.OWS * P";" * http_core.OWS * Cg(disposition_parm))^0, rawset)
+
+return {
+	Content_Disposition = Content_Disposition;
+}
diff --git a/lpeg_patterns/http/expect_ct.lua b/lpeg_patterns/http/expect_ct.lua
new file mode 100644
index 0000000..a19e927
--- /dev/null
+++ b/lpeg_patterns/http/expect_ct.lua
@@ -0,0 +1,11 @@
+-- https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-06#section-2.1
+
+local http_core = require "lpeg_patterns.http.core"
+local http_utils = require "lpeg_patterns.http.util"
+
+local expect_ct_directive = http_utils.directive
+local Expect_CT = http_utils.no_dup(http_core.comma_sep_trim(expect_ct_directive))
+
+return {
+	Expect_CT = Expect_CT;
+}
diff --git a/lpeg_patterns/http/forwarded.lua b/lpeg_patterns/http/forwarded.lua
new file mode 100644
index 0000000..603e24f
--- /dev/null
+++ b/lpeg_patterns/http/forwarded.lua
@@ -0,0 +1,17 @@
+-- RFC 7239
+-- Forwarded HTTP Extension
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+
+local P = lpeg.P
+
+-- RFC 7239 Section 4
+local value = http_core.token + http_core.quoted_string
+local forwarded_pair = http_core.token * P"=" * value
+local forwarded_element = forwarded_pair^-1 * (P";" * forwarded_pair^-1)^0
+local Forwarded = http_core.comma_sep_trim(forwarded_element)
+
+return {
+	Forwarded = Forwarded;
+}
diff --git a/lpeg_patterns/http/frameoptions.lua b/lpeg_patterns/http/frameoptions.lua
new file mode 100644
index 0000000..66eeb6d
--- /dev/null
+++ b/lpeg_patterns/http/frameoptions.lua
@@ -0,0 +1,16 @@
+-- RFC 7034
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+local util = require "lpeg_patterns.util"
+
+local case_insensitive = util.case_insensitive
+local Cc = lpeg.Cc
+
+local X_Frame_Options = case_insensitive "deny" * Cc("deny")
+	+ case_insensitive "sameorigin" * Cc("sameorigin")
+	+ case_insensitive "allow-from" * http_core.RWS * require "lpeg_patterns.http.origin".serialized_origin
+
+return {
+	X_Frame_Options = X_Frame_Options;
+}
diff --git a/lpeg_patterns/http/hoba.lua b/lpeg_patterns/http/hoba.lua
new file mode 100644
index 0000000..d9755ad
--- /dev/null
+++ b/lpeg_patterns/http/hoba.lua
@@ -0,0 +1,12 @@
+-- RFC 7486
+-- HTTP Origin-Bound Authentication (HOBA)
+
+local lpeg = require "lpeg"
+
+local C = lpeg.C
+
+local Hobareg = C"regok" + C"reginwork"
+
+return {
+	Hobareg = Hobareg;
+}
diff --git a/lpeg_patterns/http/link.lua b/lpeg_patterns/http/link.lua
new file mode 100644
index 0000000..e1c7953
--- /dev/null
+++ b/lpeg_patterns/http/link.lua
@@ -0,0 +1,30 @@
+-- RFC 5988
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+local http_parameters = require "lpeg_patterns.http.parameters"
+local uri = require "lpeg_patterns.uri"
+
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local P = lpeg.P
+local S = lpeg.S
+
+local ptokenchar = S"!#$%&'()*+-./:<=>?@[]^_`{|}~" + core.DIGIT + core.ALPHA
+local ptoken = ptokenchar^1
+local ext_name_star = http_parameters.parmname * P"*"
+local link_extension = ext_name_star * P"=" * http_parameters.ext_value
+	+ http_parameters.parmname * (P"=" * (ptoken + http_core.quoted_string))^-1
+-- See https://www.rfc-editor.org/errata_search.php?rfc=5988&eid=3158
+local link_param = link_extension
+local link_value = Cf(Ct(P"<" * uri.uri_reference * P">") * (http_core.OWS * P";" * http_core.OWS * Cg(link_param))^0, rawset)
+-- TODO: handle multiple ext_value variants...
+-- e.g. server might provide one title in english, one in chinese, client should be able to pick which one to display
+
+local Link = http_core.comma_sep_trim(link_value)
+
+return {
+	Link = Link;
+}
diff --git a/lpeg_patterns/http/origin.lua b/lpeg_patterns/http/origin.lua
new file mode 100644
index 0000000..09da2f0
--- /dev/null
+++ b/lpeg_patterns/http/origin.lua
@@ -0,0 +1,20 @@
+-- RFC 6454
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+local uri = require "lpeg_patterns.uri"
+
+local C = lpeg.C
+local P = lpeg.P
+
+-- discard captures from scheme, host, port and just get whole string
+local serialized_origin = C(uri.scheme * P"://" * uri.host * (P":" * uri.port)^-1/function() end)
+local origin_list = serialized_origin * (core.SP * serialized_origin)^0
+local origin_list_or_null = P"null" + origin_list
+local Origin = http_core.OWS * origin_list_or_null * http_core.OWS
+
+return {
+	serialized_origin = serialized_origin;
+	Origin = Origin;
+}
diff --git a/lpeg_patterns/http/parameters.lua b/lpeg_patterns/http/parameters.lua
new file mode 100644
index 0000000..f067940
--- /dev/null
+++ b/lpeg_patterns/http/parameters.lua
@@ -0,0 +1,29 @@
+-- RFC 5987
+-- Character Set and Language Encoding for
+-- Hypertext Transfer Protocol (HTTP) Header Field Parameters
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local language = require "lpeg_patterns.language"
+local util = require "lpeg_patterns.util"
+
+local C = lpeg.C
+local Cg = lpeg.Cg
+local Cs = lpeg.Cs
+local P = lpeg.P
+local S = lpeg.S
+
+local attr_char = core.ALPHA + core.DIGIT + S"!#$&+-.^_`|~"
+-- can't use uri.pct_encoded, as it doesn't decode all characters
+local pct_encoded = P"%" * (core.HEXDIG * core.HEXDIG / util.read_hex) / string.char
+local value_chars = Cs((pct_encoded + attr_char)^0)
+local parmname = C(attr_char^1)
+-- ext-value uses charset from RFC 5987
+local mime_charsetc = core.ALPHA + core.DIGIT + S"!#$%&+-^_`{}~"
+local mime_charset = C(mime_charsetc^1)
+local ext_value = Cg(mime_charset, "charset") * P"'" * Cg(language.Language_Tag, "language")^-1 * P"'" * value_chars
+
+return {
+	ext_value = ext_value;
+	parmname = parmname;
+}
diff --git a/lpeg_patterns/http/pkp.lua b/lpeg_patterns/http/pkp.lua
new file mode 100644
index 0000000..e3c7393
--- /dev/null
+++ b/lpeg_patterns/http/pkp.lua
@@ -0,0 +1,54 @@
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+local http_utils = require "lpeg_patterns.http.util"
+
+local Cmt = lpeg.Cmt
+local P = lpeg.P
+
+-- RFC 7469
+local Public_Key_Directives = http_utils.directive * (http_core.OWS * P";" * http_core.OWS * http_utils.directive)^0
+local function pkp_cmt(pins, t, k, v, ...)
+	-- duplicates are allowed if the directive name starts with "pin-"
+	local pin_name = k:match("^pin%-(.+)")
+	if pin_name then
+		local hashes = pins[pin_name]
+		if hashes then
+			hashes[#hashes+1] = v
+		else
+			hashes = {v}
+			pins[pin_name] = hashes
+		end
+	else
+		local old = t[k]
+		if old then
+			return false
+		end
+		t[k] = v
+	end
+	if ... then
+		return pkp_cmt(pins, t, ...)
+	else
+		return true
+	end
+end
+local Public_Key_Pins = Cmt(Public_Key_Directives, function(_, _, ...)
+	local pins = {}
+	local t = {}
+	local ok = pkp_cmt(pins, t, ...)
+	if ok and t["max-age"] then
+		return true, pins, t
+	end
+end)
+local Public_Key_Pins_Report_Only = Cmt(Public_Key_Directives, function(_, _, ...)
+	local pins = {}
+	local t = {}
+	local ok = pkp_cmt(pins, t, ...)
+	if ok then
+		return true, pins, t
+	end
+end)
+
+return {
+	Public_Key_Pins = Public_Key_Pins;
+	Public_Key_Pins_Report_Only = Public_Key_Pins_Report_Only;
+}
diff --git a/lpeg_patterns/http/range.lua b/lpeg_patterns/http/range.lua
new file mode 100644
index 0000000..d979881
--- /dev/null
+++ b/lpeg_patterns/http/range.lua
@@ -0,0 +1,53 @@
+-- RFC 7233
+-- Hypertext Transfer Protocol (HTTP/1.1): Range Requests
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_conditional = require "lpeg_patterns.http.conditional"
+local http_core = require "lpeg_patterns.http.core"
+local http_semantics = require "lpeg_patterns.http.semantics"
+
+local C = lpeg.C
+local Cc = lpeg.Cc
+local P = lpeg.P
+
+local bytes_unit = P"bytes"
+local other_range_unit = http_core.token
+local range_unit = C(bytes_unit) + other_range_unit
+
+local first_byte_pos = core.DIGIT^1 / tonumber
+local last_byte_pos = core.DIGIT^1 / tonumber
+local byte_range_spec = first_byte_pos * P"-" * last_byte_pos^-1
+local suffix_length = core.DIGIT^1 / tonumber
+local suffix_byte_range_spec = Cc(nil) * P"-" * suffix_length
+local byte_range_set = http_core.comma_sep(byte_range_spec + suffix_byte_range_spec, 1)
+local byte_ranges_specifier = bytes_unit * P"=" * byte_range_set
+
+-- RFC 7233 Section 2.3
+local acceptable_ranges = http_core.comma_sep_trim(range_unit, 1) + P"none"
+local Accept_Ranges = acceptable_ranges
+
+-- RFC 7233 Section 3.1
+local other_range_set = core.VCHAR^1
+local other_ranges_specifier = other_range_unit * P"=" * other_range_set
+local Range = byte_ranges_specifier + other_ranges_specifier
+
+-- RFC 7233 Section 3.2
+local If_Range = http_conditional.entity_tag + http_semantics.HTTP_date
+
+-- RFC 7233 Section 4.2
+local complete_length = core.DIGIT^1 / tonumber
+local unsatisfied_range = P"*/" * complete_length
+local byte_range = first_byte_pos * P"-" * last_byte_pos
+local byte_range_resp = byte_range * P"/" * (complete_length + P"*")
+local byte_content_range = bytes_unit * core.SP * (byte_range_resp + unsatisfied_range)
+local other_range_resp = core.CHAR^0
+local other_content_range = other_range_unit * core.SP * other_range_resp
+local Content_Range = byte_content_range + other_content_range
+
+return {
+	Accept_Ranges = Accept_Ranges;
+	Range = Range;
+	If_Range = If_Range;
+	Content_Range = Content_Range;
+}
diff --git a/lpeg_patterns/http/referrer_policy.lua b/lpeg_patterns/http/referrer_policy.lua
new file mode 100644
index 0000000..0b2be18
--- /dev/null
+++ b/lpeg_patterns/http/referrer_policy.lua
@@ -0,0 +1,20 @@
+-- https://www.w3.org/TR/referrer-policy/#referrer-policy-header
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+
+local C = lpeg.C
+
+local policy_token = C"no-referrer"
+	+ C"no-referrer-when-downgrade"
+	+ C"strict-origin"
+	+ C"strict-origin-when-cross-origin"
+	+ C"same-origin"
+	+ C"origin"
+	+ C"origin-when-cross-origin"
+	+ C"unsafe-url"
+local Referrer_Policy = http_core.comma_sep_trim(policy_token, 1)
+
+return {
+	Referrer_Policy = Referrer_Policy;
+}
diff --git a/lpeg_patterns/http/semantics.lua b/lpeg_patterns/http/semantics.lua
new file mode 100644
index 0000000..015fc6a
--- /dev/null
+++ b/lpeg_patterns/http/semantics.lua
@@ -0,0 +1,186 @@
+-- RFC 7231
+-- Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+local email = require "lpeg_patterns.email"
+local language = require "lpeg_patterns.language"
+local uri = require "lpeg_patterns.uri"
+
+local Cc = lpeg.Cc
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local P = lpeg.P
+local S = lpeg.S
+
+
+-- RFC 7231 Section 3.1.1
+local content_coding = http_core.token / string.lower -- case insensitive
+local Content_Encoding = http_core.comma_sep_trim(content_coding, 1)
+
+-- RFC 7231 Section 3.1.2
+local type = http_core.token / string.lower -- case insensitive
+local subtype = http_core.token / string.lower -- case insensitive
+local parameter = http_core.token / string.lower -- case insensitive
+	* P"=" * (http_core.token + http_core.quoted_string)
+local media_type = Cg(type, "type") * P"/" * Cg(subtype, "subtype")
+	* Cg(Cf(Ct(true) * (http_core.OWS * P";" * http_core.OWS * Cg(parameter))^0, rawset), "parameters")
+local charset = http_core.token / string.lower -- case insensitive
+local Content_Type = Ct(media_type)
+
+-- RFC 7231 Section 3.1.3
+local Content_Language = http_core.comma_sep_trim(language.Language_Tag, 1)
+
+-- RFC 7231 Section 3.1.4.2
+local Content_Location = uri.absolute_uri + http_core.partial_uri
+
+-- RFC 7231 Section 5.1.1
+local Expect = P"100-"*S"cC"*S"oO"*S"nN"*S"tT"*S"iI"*S"nN"*S"uU"*S"eE" * Cc("100-continue")
+
+-- RFC 7231 Section 5.1.2
+local Max_Forwards = core.DIGIT^1 / tonumber
+
+-- RFC 7231 Section 5.3.1
+-- local qvalue = http_core.rank -- luacheck: ignore 211
+local weight = http_core.t_ranking
+
+-- RFC 7231 Section 5.3.2
+local media_range = (P"*/*"
+	+ (Cg(type, "type") * P"/*")
+	+ (Cg(type, "type") * P"/" * Cg(subtype, "subtype"))
+) * Cg(Cf(Ct(true) * (http_core.OWS * ";" * http_core.OWS * Cg(parameter) - weight)^0, rawset), "parameters")
+local accept_ext = http_core.OWS * P";" * http_core.OWS * http_core.token * (P"=" * (http_core.token + http_core.quoted_string))^-1
+local accept_params = Cg(weight, "q") * Cg(Cf(Ct(true) * Cg(accept_ext)^0, rawset), "extensions")
+local Accept = http_core.comma_sep_trim(Ct(media_range * (accept_params+Cg(Ct(true), "extensions"))))
+
+-- RFC 7231 Section 5.3.3
+local Accept_Charset = http_core.comma_sep_trim((charset + P"*") * weight^-1, 1)
+
+-- RFC 7231 Section 5.3.4
+local codings = content_coding + "*"
+local Accept_Encoding = http_core.comma_sep_trim(codings * weight^-1)
+
+-- RFC 4647 Section 2.1
+local alphanum = core.ALPHA + core.DIGIT
+local language_range = (core.ALPHA * core.ALPHA^-7 * (P"-" * alphanum * alphanum^-7)^0) + P"*"
+-- RFC 7231 Section 5.3.5
+local Accept_Language = http_core.comma_sep_trim(language_range * weight^-1, 1)
+
+-- RFC 7231 Section 5.5.1
+local From = email.mailbox
+
+-- RFC 7231 Section 5.5.2
+local Referer = uri.absolute_uri + http_core.partial_uri
+
+-- RFC 7231 Section 5.5.3
+local product_version = http_core.token
+local product = http_core.token * (P"/" * product_version)^-1
+local User_Agent = product * (http_core.RWS * (product + http_core.comment))^0
+
+-- RFC 7231 Section 7.1.1.1
+-- Uses os.date field names
+local day_name = Cg(P"Mon"*Cc(2)
+	+ P"Tue"*Cc(3)
+	+ P"Wed"*Cc(4)
+	+ P"Thu"*Cc(5)
+	+ P"Fri"*Cc(6)
+	+ P"Sat"*Cc(7)
+	+ P"Sun"*Cc(1), "wday")
+local day = Cg(core.DIGIT * core.DIGIT / tonumber, "day")
+local month = Cg(P"Jan"*Cc(1)
+	+ P"Feb"*Cc(2)
+	+ P"Mar"*Cc(3)
+	+ P"Apr"*Cc(4)
+	+ P"May"*Cc(5)
+	+ P"Jun"*Cc(6)
+	+ P"Jul"*Cc(7)
+	+ P"Aug"*Cc(8)
+	+ P"Sep"*Cc(9)
+	+ P"Oct"*Cc(10)
+	+ P"Nov"*Cc(11)
+	+ P"Dec"*Cc(12), "month")
+local year = Cg(core.DIGIT * core.DIGIT * core.DIGIT * core.DIGIT / tonumber, "year")
+local date1 = day * core.SP * month * core.SP * year
+
+local GMT = P"GMT"
+
+local minute = Cg(core.DIGIT * core.DIGIT / tonumber, "min")
+local second = Cg(core.DIGIT * core.DIGIT / tonumber, "sec")
+local hour = Cg(core.DIGIT * core.DIGIT / tonumber, "hour")
+-- XXX only match 00:00:00 - 23:59:60 (leap second)?
+
+local time_of_day = hour * P":" * minute * P":" * second
+local IMF_fixdate = Ct(day_name * P"," * core.SP * date1 * core.SP * time_of_day * core.SP * GMT)
+
+local date2 do
+	local year_barrier = 70
+	local twodayyear = Cg(core.DIGIT * core.DIGIT / function(y)
+		y = tonumber(y, 10)
+		if y < year_barrier then
+			return 2000+y
+		else
+			return 1900+y
+		end
+	end, "year")
+	date2 = day * P"-" * month * P"-" * twodayyear
+end
+local day_name_l = Cg(P"Monday"*Cc(2)
+	+ P"Tuesday"*Cc(3)
+	+ P"Wednesday"*Cc(4)
+	+ P"Thursday"*Cc(5)
+	+ P"Friday"*Cc(6)
+	+ P"Saturday"*Cc(7)
+	+ P"Sunday"*Cc(1), "wday")
+local rfc850_date = Ct(day_name_l * P"," * core.SP * date2 * core.SP * time_of_day * core.SP * GMT)
+
+local date3 = month * core.SP * (day + Cg(core.SP * core.DIGIT / tonumber, "day"))
+local asctime_date = Ct(day_name * core.SP * date3 * core.SP * time_of_day * core.SP * year)
+local obs_date = rfc850_date + asctime_date
+
+local HTTP_date = IMF_fixdate + obs_date
+local Date = HTTP_date
+
+-- RFC 7231 Section 7.1.2
+local Location = uri.uri_reference
+
+-- RFC 7231 Section 7.1.3
+local delay_seconds = core.DIGIT^1 / tonumber
+local Retry_After = HTTP_date + delay_seconds
+
+-- RFC 7231 Section 7.1.4
+local Vary = P"*" + http_core.comma_sep(http_core.field_name, 1)
+
+-- RFC 7231 Section 7.4.1
+local Allow = http_core.comma_sep_trim(http_core.method)
+
+-- RFC 7231 Section 7.4.2
+local Server = product * (http_core.RWS * (product + http_core.comment))^0
+
+return {
+	HTTP_date = HTTP_date;
+	IMF_fixdate = IMF_fixdate;
+	media_type = media_type;
+	parameter = parameter;
+
+	Accept = Accept;
+	Accept_Charset = Accept_Charset;
+	Accept_Encoding = Accept_Encoding;
+	Accept_Language = Accept_Language;
+	Allow = Allow;
+	Content_Encoding = Content_Encoding;
+	Content_Language = Content_Language;
+	Content_Location = Content_Location;
+	Content_Type = Content_Type;
+	Date = Date;
+	Expect = Expect;
+	From = From;
+	Location = Location;
+	Max_Forwards = Max_Forwards;
+	Referer = Referer;
+	Retry_After = Retry_After;
+	Server = Server;
+	User_Agent = User_Agent;
+	Vary = Vary;
+}
diff --git a/lpeg_patterns/http/slug.lua b/lpeg_patterns/http/slug.lua
new file mode 100644
index 0000000..06cf9af
--- /dev/null
+++ b/lpeg_patterns/http/slug.lua
@@ -0,0 +1,20 @@
+-- RFC 5023
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+local util = require "lpeg_patterns.util"
+
+local Cs = lpeg.Cs
+local P = lpeg.P
+local R = lpeg.R
+
+local slugtext = http_core.RWS / " "
+	+ P"%" * (core.HEXDIG * core.HEXDIG / util.read_hex) / string.char
+	+ R"\32\126"
+
+local SLUG = Cs(slugtext^0)
+
+return {
+	SLUG = SLUG;
+}
diff --git a/lpeg_patterns/http/sts.lua b/lpeg_patterns/http/sts.lua
new file mode 100644
index 0000000..3732b68
--- /dev/null
+++ b/lpeg_patterns/http/sts.lua
@@ -0,0 +1,13 @@
+-- RFC 6797
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+local http_utils = require "lpeg_patterns.http.util"
+
+local P = lpeg.P
+
+local Strict_Transport_Security = http_utils.no_dup(http_utils.directive^-1 * (http_core.OWS * P";" * http_core.OWS * http_utils.directive^-1)^0)
+
+return {
+	Strict_Transport_Security = Strict_Transport_Security;
+}
diff --git a/lpeg_patterns/http/util.lua b/lpeg_patterns/http/util.lua
new file mode 100644
index 0000000..0cf0ec8
--- /dev/null
+++ b/lpeg_patterns/http/util.lua
@@ -0,0 +1,38 @@
+-- This is a private module containing utility functions shared by various http parsers
+
+local lpeg = require "lpeg"
+local http_core = require "lpeg_patterns.http.core"
+
+local P = lpeg.P
+local Cc = lpeg.Cc
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local Cmt = lpeg.Cmt
+
+local directive_name = http_core.token / string.lower
+local directive_value = http_core.token + http_core.quoted_string
+local directive = Cg(directive_name * ((http_core.OWS * P"=" * http_core.OWS * directive_value) + Cc(true)))
+
+-- Helper function that doesn't match if there are duplicate keys
+local function no_dup_cmt(s, i, t, name, value, ...)
+	local old = t[name]
+	if old then
+		return false
+	end
+	t[name] = value
+	if ... then
+		return no_dup_cmt(s, i, t, ...)
+	elseif t["max-age"] then -- max-age is required
+		return true, t
+	end
+	-- else return nil
+end
+
+local function no_dup(patt)
+	return Cmt(Ct(true) * patt, no_dup_cmt)
+end
+
+return {
+	directive = directive;
+	no_dup = no_dup;
+}
diff --git a/lpeg_patterns/http/webdav.lua b/lpeg_patterns/http/webdav.lua
new file mode 100644
index 0000000..3e99c07
--- /dev/null
+++ b/lpeg_patterns/http/webdav.lua
@@ -0,0 +1,67 @@
+-- WebDAV
+
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_conditional = require "lpeg_patterns.http.conditional"
+local http_core = require "lpeg_patterns.http.core"
+local uri = require "lpeg_patterns.uri"
+local util = require "lpeg_patterns.util"
+
+local case_insensitive = util.case_insensitive
+
+local Cc = lpeg.Cc
+local P = lpeg.P
+local S = lpeg.S
+
+local T_F = S"Tt" * Cc(true) + S"Ff" * Cc(false)
+
+-- RFC 4918
+local Coded_URL = P"<" * uri.absolute_uri * P">"
+local extend = Coded_URL + http_core.token
+local compliance_class = P"1" + P"2" + P"3" + extend
+local DAV = http_core.comma_sep_trim(compliance_class)
+local Depth = P"0" * Cc(0)
+	+ P"1" * Cc(1)
+	+ case_insensitive "infinity" * Cc(math.huge)
+local Simple_ref = uri.absolute_uri + http_core.partial_uri
+local Destination = Simple_ref
+local State_token = Coded_URL
+local Condition = (case_insensitive("not") * Cc("not"))^-1
+	* http_core.OWS * (State_token + P"[" * http_conditional.entity_tag * P"]")
+local List = P"(" * http_core.OWS * (Condition * http_core.OWS)^1 * P")"
+local No_tag_list = List
+local Resource_Tag = P"<" * Simple_ref * P">"
+local Tagged_list = Resource_Tag * http_core.OWS * (List * http_core.OWS)^1
+local If = (Tagged_list * http_core.OWS)^1 + (No_tag_list * http_core.OWS)^1
+local Lock_Token = Coded_URL
+local Overwrite = T_F
+local DAVTimeOutVal = core.DIGIT^1 / tonumber
+local TimeType = case_insensitive "Second-" * DAVTimeOutVal
+	+ case_insensitive "Infinite" * Cc(math.huge)
+local TimeOut = http_core.comma_sep_trim(TimeType)
+
+-- RFC 5323
+local DASL = http_core.comma_sep_trim(Coded_URL, 1)
+
+-- RFC 6638
+local Schedule_Reply = T_F
+local Schedule_Tag = http_conditional.opaque_tag
+local If_Schedule_Tag_Match = http_conditional.opaque_tag
+
+-- RFC 7809
+local CalDAV_Timezones = T_F
+
+return {
+	CalDAV_Timezones = CalDAV_Timezones;
+	DASL = DASL;
+	DAV = DAV;
+	Depth = Depth;
+	Destination = Destination;
+	If = If;
+	If_Schedule_Tag_Match = If_Schedule_Tag_Match;
+	Lock_Token = Lock_Token;
+	Overwrite = Overwrite;
+	Schedule_Reply = Schedule_Reply;
+	Schedule_Tag = Schedule_Tag;
+	TimeOut = TimeOut;
+}
diff --git a/lpeg_patterns/http/websocket.lua b/lpeg_patterns/http/websocket.lua
new file mode 100644
index 0000000..51e723e
--- /dev/null
+++ b/lpeg_patterns/http/websocket.lua
@@ -0,0 +1,55 @@
+local lpeg = require "lpeg"
+local core = require "lpeg_patterns.core"
+local http_core = require "lpeg_patterns.http.core"
+
+local Cc = lpeg.Cc
+local Cf = lpeg.Cf
+local Cg = lpeg.Cg
+local Ct = lpeg.Ct
+local Cmt = lpeg.Cmt
+local P = lpeg.P
+local S = lpeg.S
+
+-- RFC 6455
+local base64_character = core.ALPHA + core.DIGIT + S"+/"
+local base64_data = base64_character * base64_character * base64_character * base64_character
+local base64_padding = base64_character * base64_character * P"=="
+	+ base64_character * base64_character * base64_character * P"="
+local base64_value_non_empty = (base64_data^1 * base64_padding^-1) + base64_padding
+local Sec_WebSocket_Accept = base64_value_non_empty
+local Sec_WebSocket_Key = base64_value_non_empty
+local registered_token = http_core.token
+local extension_token = registered_token
+local extension_param do
+	local EOF = P(-1)
+	local token_then_EOF = Cc(true) * http_core.token * EOF
+	-- the quoted-string must be a valid token
+	local quoted_token = Cmt(http_core.quoted_string, function(_, _, q)
+		return token_then_EOF:match(q)
+	end)
+	extension_param = http_core.token * ((P"=" * (http_core.token + quoted_token)) + Cc(true))
+end
+local extension = extension_token * Cg(Cf(Ct(true) * (P";" * Cg(extension_param))^0, rawset), "parameters")
+local extension_list = http_core.comma_sep_trim(Ct(extension))
+local Sec_WebSocket_Extensions = extension_list
+local Sec_WebSocket_Protocol_Client = http_core.comma_sep_trim(http_core.token)
+local Sec_WebSocket_Protocol_Server = http_core.token
+local NZDIGIT =  S"123456789"
+-- Limited to 0-255 range, with no leading zeros
+local version = (
+	P"2" * (S"01234" * core.DIGIT + P"5" * S"012345")
+	+ (P"1") * core.DIGIT * core.DIGIT
+	+ NZDIGIT * core.DIGIT^-1
+) / tonumber
+local Sec_WebSocket_Version_Client = version
+local Sec_WebSocket_Version_Server = http_core.comma_sep_trim(version)
+
+return {
+	Sec_WebSocket_Accept = Sec_WebSocket_Accept;
+	Sec_WebSocket_Key = Sec_WebSocket_Key;
+	Sec_WebSocket_Extensions = Sec_WebSocket_Extensions;
+	Sec_WebSocket_Protocol_Client = Sec_WebSocket_Protocol_Client;
+	Sec_WebSocket_Protocol_Server = Sec_WebSocket_Protocol_Server;
+	Sec_WebSocket_Version_Client = Sec_WebSocket_Version_Client;
+	Sec_WebSocket_Version_Server = Sec_WebSocket_Version_Server;
+}
diff --git a/lpeg_patterns/phone.lua b/lpeg_patterns/phone.lua
index 19776fe..22836d0 100644
--- a/lpeg_patterns/phone.lua
+++ b/lpeg_patterns/phone.lua
@@ -24,7 +24,8 @@ _M.Australia = (
 		* seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit
 	-- Local rate calls
 	+ P"1300" * seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit
-	+ P"1345" * seperator^-1 * digit*digit * seperator^-1 * digit*digit --only used for back-to-base monitored alarm systems
+	-- 1345 is only used for back-to-base monitored alarm systems
+	+ P"1345" * seperator^-1 * digit*digit * seperator^-1 * digit*digit
 	+ P"13"   * seperator^-1 * digit*digit * seperator^-1 * digit*digit
 	+ (P"0")^-1*P"198" * seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit -- data calls
 	-- Free calls
diff --git a/lpeg_patterns/util.lua b/lpeg_patterns/util.lua
index ae3c8ae..a612503 100644
--- a/lpeg_patterns/util.lua
+++ b/lpeg_patterns/util.lua
@@ -1,3 +1,21 @@
+local lpeg = require "lpeg"
+local C = lpeg.C
+local P = lpeg.P
+local S = lpeg.S
+
+local function case_insensitive(str)
+	local patt = P(true)
+	for i=1, #str do
+		local c = str:sub(i, i)
+		patt = patt * S(c:upper() .. c:lower())
+	end
+	return patt
+end
+
+local function no_rich_capture(patt)
+	return C(patt) / function(a) return a end
+end
+
 local function read_hex(hex_num)
 	return tonumber(hex_num, 16)
 end
@@ -30,6 +48,8 @@ local safe_tonumber do -- locale independent tonumber function
 end
 
 return {
+	case_insensitive = case_insensitive;
+	no_rich_capture = no_rich_capture;
 	read_hex = read_hex;
 	safe_tonumber = safe_tonumber;
 }
diff --git a/spec/email_spec.lua b/spec/email_spec.lua
index aed530c..4d2943d 100644
--- a/spec/email_spec.lua
+++ b/spec/email_spec.lua
@@ -17,7 +17,8 @@ describe("email Addresses", function()
 		assert.same({[[quoted]], "example.com"}, email:match [["quoted"@example.com]])
 		assert.same({[[quoted string]], "example.com"}, email:match [["quoted string"@example.com]])
 		assert.same({[[quoted@symbol]], "example.com"}, email:match [["quoted@symbol"@example.com]])
-		assert.same({[=[very.(),:;<>[]".VERY."very@\ "very".unusual]=], "example.com"}, email:match [=["very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@example.com]=])
+		assert.same({[=[very.(),:;<>[]".VERY."very@\ "very".unusual]=], "example.com"},
+			email:match [=["very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@example.com]=])
 	end)
 	it("folds whitespace", function()
 		assert.same({"localpart ", "example.com"}, email:match [["localpart "@example.com]])
@@ -37,7 +38,8 @@ describe("email Addresses", function()
 		assert.falsy(email:match "foobar.@example.com")
 		assert.falsy(email:match "@foo@example.com")
 		assert.falsy(email:match "foo@bar@example.com")
-		assert.falsy(email:match [[just"not"right@example.com]]) -- quoted strings must be dot separated, or the only element making up the local-pat
+		-- quoted strings must be dot separated, or the only element making up the local-pat
+		assert.falsy(email:match [[just"not"right@example.com]])
 		assert.falsy(email:match "\127@example.com")
 	end)
 	it("Handle unusual hosts", function()
@@ -107,7 +109,8 @@ describe("email nocfws variants", function()
 		assert.same({[[quoted]], "example.com"}, email_nocfws:match [["quoted"@example.com]])
 		assert.same({[[quoted string]], "example.com"}, email_nocfws:match [["quoted string"@example.com]])
 		assert.same({[[quoted@symbol]], "example.com"}, email_nocfws:match [["quoted@symbol"@example.com]])
-		assert.same({[=[very.(),:;<>[]".VERY."very@\ "very".unusual]=], "example.com"}, email_nocfws:match [=["very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@example.com]=])
+		assert.same({[=[very.(),:;<>[]".VERY."very@\ "very".unusual]=], "example.com"},
+			email_nocfws:match [=["very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@example.com]=])
 	end)
 	it("Ignore invalid localpart", function()
 		assert.falsy(email_nocfws:match "@example.com")
@@ -115,7 +118,8 @@ describe("email nocfws variants", function()
 		assert.falsy(email_nocfws:match "foobar.@example.com")
 		assert.falsy(email_nocfws:match "@foo@example.com")
 		assert.falsy(email_nocfws:match "foo@bar@example.com")
-		assert.falsy(email_nocfws:match [[just"not"right@example.com]]) -- quoted strings must be dot separated, or the only element making up the local-pat
+		-- quoted strings must be dot separated, or the only element making up the local-pat
+		assert.falsy(email_nocfws:match [[just"not"right@example.com]])
 		assert.falsy(email_nocfws:match "\127@example.com")
 	end)
 	it("Handle unusual hosts", function()
@@ -143,10 +147,13 @@ describe("mailbox", function()
 		assert.same({"foo", "example.com", display = "Foo"}, mailbox:match "Foo<foo@example.com>")
 		assert.same({"foo", "example.com", display = "Foo "}, mailbox:match "Foo <foo@example.com>")
 		assert.same({"foo", "example.com", display = [["Foo"]]}, mailbox:match [["Foo"<foo@example.com>]])
-		assert.same({"foo", "example.com", display = "Old.Style.With.Dots"}, mailbox:match "Old.Style.With.Dots<foo@example.com>")
-		assert.same({"foo", "example.com", display = "Multiple Words"}, mailbox:match "Multiple Words<foo@example.com>")
+		assert.same({"foo", "example.com", display = "Old.Style.With.Dots"},
+			mailbox:match "Old.Style.With.Dots<foo@example.com>")
+		assert.same({"foo", "example.com", display = "Multiple Words"},
+			mailbox:match "Multiple Words<foo@example.com>")
 	end)
 	it("matches a old school name-addr", function()
-		assert.same({"foo", "example.com", route = {"wow", "such", "domains"}}, mailbox:match "<@wow,@such,,@domains:foo@example.com>")
+		assert.same({"foo", "example.com", route = {"wow", "such", "domains"}},
+			mailbox:match "<@wow,@such,,@domains:foo@example.com>")
 	end)
 end)
diff --git a/spec/http_alpn_spec.lua b/spec/http_alpn_spec.lua
new file mode 100644
index 0000000..401674f
--- /dev/null
+++ b/spec/http_alpn_spec.lua
@@ -0,0 +1,22 @@
+describe("lpeg_patterns.http.alpn", function()
+	local http_alpn = require "lpeg_patterns.http.alpn"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	local protocol_id = http_alpn.protocol_id * EOF
+	it("unescapes an ALPN protocol id correctly", function()
+		assert.same("foo", protocol_id:match("foo"))
+		-- percent encoded chars
+		assert.same(" ", protocol_id:match("%20")) -- space
+		assert.same("%", protocol_id:match("%25")) -- %
+	end)
+	it("must not decode to character that didn't need to be escaped", function()
+		assert.same(nil, protocol_id:match("%41")) -- a
+		assert.same(nil, protocol_id:match("%26")) -- &
+	end)
+	it("must be 2 digit hex", function()
+		assert.same(nil, protocol_id:match("%2"))
+	end)
+	it("must be uppercase hex", function()
+		assert.same(nil, protocol_id:match("%1a"))
+	end)
+end)
diff --git a/spec/http_cookie_spec.lua b/spec/http_cookie_spec.lua
new file mode 100644
index 0000000..08fcb00
--- /dev/null
+++ b/spec/http_cookie_spec.lua
@@ -0,0 +1,32 @@
+describe("lpeg_patterns.http.cookie", function()
+	local http_cookie = require "lpeg_patterns.http.cookie"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a Set-Cookie header", function()
+		local Set_Cookie = lpeg.Ct(http_cookie.Set_Cookie) * EOF
+		assert.same({"SID", "31d4d96e407aad42", {}}, Set_Cookie:match"SID=31d4d96e407aad42")
+		assert.same({"SID", "", {}}, Set_Cookie:match"SID=")
+		assert.same({"SID", "31d4d96e407aad42", {path="/"; domain="example.com"}},
+			Set_Cookie:match"SID=31d4d96e407aad42; Path=/; Domain=example.com")
+		assert.same({"SID", "31d4d96e407aad42", {
+			path = "/";
+			domain = "example.com";
+			secure = true;
+			expires = "Sun Nov  6 08:49:37 1994";
+		}}, Set_Cookie:match"SID=31d4d96e407aad42; Path=/; Domain=example.com; Secure; Expires=Sun Nov  6 08:49:37 1994")
+		-- Space before '='
+		assert.same({"SID", "31d4d96e407aad42", {path = "/";}}, Set_Cookie:match"SID=31d4d96e407aad42; Path =/")
+		-- Quoted cookie value
+		assert.same({"SID", "31d4d96e407aad42", {path = "/";}}, Set_Cookie:match[[SID="31d4d96e407aad42"; Path=/]])
+		-- Crazy whitespace
+		assert.same({"SID", "31d4d96e407aad42", {path = "/";}}, Set_Cookie:match"SID  =   31d4d96e407aad42  ;   Path  =  /")
+		assert.same({"SID", "31d4d96e407aad42", {["foo  bar"] = true;}},
+			Set_Cookie:match"SID  =   31d4d96e407aad42  ;  foo  bar")
+	end)
+	it("Parses a Cookie header", function()
+		local Cookie = http_cookie.Cookie * EOF
+		assert.same({SID = "31d4d96e407aad42"}, Cookie:match"SID=31d4d96e407aad42")
+		assert.same({SID = "31d4d96e407aad42"}, Cookie:match"SID = 31d4d96e407aad42")
+		assert.same({SID = "31d4d96e407aad42", lang = "en-US"}, Cookie:match"SID=31d4d96e407aad42; lang=en-US")
+	end)
+end)
diff --git a/spec/http_disposition_spec.lua b/spec/http_disposition_spec.lua
new file mode 100644
index 0000000..07df840
--- /dev/null
+++ b/spec/http_disposition_spec.lua
@@ -0,0 +1,11 @@
+describe("lpeg_patterns.http.disposition", function()
+	local http_disposition = require "lpeg_patterns.http.disposition"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a Content-Disposition header", function()
+		local Content_Disposition = lpeg.Ct(http_disposition.Content_Disposition) * EOF
+		assert.same({"foo", {}}, Content_Disposition:match"foo")
+		assert.same({"foo", {filename="example"}}, Content_Disposition:match"foo; filename=example")
+		assert.same({"foo", {filename="example"}}, Content_Disposition:match"foo; filename*=UTF-8''example")
+	end)
+end)
diff --git a/spec/http_expect_ct_spec.lua b/spec/http_expect_ct_spec.lua
new file mode 100644
index 0000000..e761b9d
--- /dev/null
+++ b/spec/http_expect_ct_spec.lua
@@ -0,0 +1,18 @@
+describe("lpeg_patterns.http.expect_ct", function()
+	local http_expect_ct = require "lpeg_patterns.http.expect_ct"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a Expect-Ct header", function()
+		-- Examples from draft-ietf-httpbis-expect-ct-06 2.1.4
+		local sts_patt = http_expect_ct.Expect_CT * EOF
+		assert.same({["max-age"] = "86400", enforce = true}, sts_patt:match("max-age=86400, enforce"))
+		assert.same({
+			["max-age"] = "86400";
+			["report-uri"] = "https://foo.example/report";
+		}, sts_patt:match([[max-age=86400,report-uri="https://foo.example/report"]]))
+		-- max-age is required
+		assert.same(nil, sts_patt:match("foo=0"))
+		-- Should fail to parse when duplicate field given
+		assert.same(nil, sts_patt:match("max-age086400, foo=0, foo=1"))
+	end)
+end)
diff --git a/spec/http_frameoptions_spec.lua b/spec/http_frameoptions_spec.lua
new file mode 100644
index 0000000..601b5c6
--- /dev/null
+++ b/spec/http_frameoptions_spec.lua
@@ -0,0 +1,12 @@
+describe("lpeg_patterns.http.frameoptions", function()
+	local http_frameoptions = require "lpeg_patterns.http.frameoptions"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses an X-Frame-Options header", function()
+		local X_Frame_Options = lpeg.Ct(http_frameoptions.X_Frame_Options) * EOF
+		assert.same({"deny"}, X_Frame_Options:match("deny"))
+		assert.same({"deny"}, X_Frame_Options:match("DENY"))
+		assert.same({"deny"}, X_Frame_Options:match("dEnY"))
+		assert.same({"http://example.com"}, X_Frame_Options:match("Allow-From http://example.com"))
+	end)
+end)
diff --git a/spec/http_link_spec.lua b/spec/http_link_spec.lua
new file mode 100644
index 0000000..b26b245
--- /dev/null
+++ b/spec/http_link_spec.lua
@@ -0,0 +1,25 @@
+describe("lpeg_patterns.http.link", function()
+	local http_link = require "lpeg_patterns.http.link"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a Link header", function()
+		local Link = lpeg.Ct(http_link.Link) * EOF
+		assert.same({{{host="example.com"}}}, Link:match"<//example.com>")
+		assert.same({
+			{
+				{scheme = "http"; host = "example.com"; path = "/TheBook/chapter2";};
+				rel = "previous";
+				title="previous chapter"
+			}},
+			Link:match[[<http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"]])
+		assert.same({{{path = "/"}, rel = "http://example.net/foo"}},
+			Link:match[[</>; rel="http://example.net/foo"]])
+		assert.same({
+				{{path = "/TheBook/chapter2"}, rel = "previous", title = "letztes Kapitel"};
+				{{path = "/TheBook/chapter4"}, rel = "next", title = "nächstes Kapitel"};
+			},
+			Link:match[[</TheBook/chapter2>; rel="previous"; title*=UTF-8'de'letztes%20Kapitel, </TheBook/chapter4>; rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel]])
+		assert.same({{{scheme = "http"; host = "example.org"; path = "/"}, rel = "start http://example.net/relation/other"}},
+			Link:match[[<http://example.org/>; rel="start http://example.net/relation/other"]])
+	end)
+end)
diff --git a/spec/http_origin_spec.lua b/spec/http_origin_spec.lua
new file mode 100644
index 0000000..151764c
--- /dev/null
+++ b/spec/http_origin_spec.lua
@@ -0,0 +1,11 @@
+describe("lpeg_patterns.http.origin", function()
+	local http_origin = require "lpeg_patterns.http.origin"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses an Origin header", function()
+		local Origin = lpeg.Ct(http_origin.Origin) * EOF
+		assert.same({}, Origin:match("null"))
+		assert.same({"http://example.com"}, Origin:match("http://example.com"))
+		assert.same({"http://example.com", "https://foo.org"}, Origin:match("http://example.com https://foo.org"))
+	end)
+end)
diff --git a/spec/http_pkp_spec.lua b/spec/http_pkp_spec.lua
new file mode 100644
index 0000000..3d9b943
--- /dev/null
+++ b/spec/http_pkp_spec.lua
@@ -0,0 +1,46 @@
+describe("lpeg_patterns.http.pkp", function()
+	local http_pkp = require "lpeg_patterns.http.pkp"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a HPKP header", function()
+		-- Example from RFC 7469 2.1.5
+		local pkp_patt = lpeg.Ct(http_pkp.Public_Key_Pins) * EOF
+		assert.same({
+			{
+				sha256 = {
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
+				};
+			}, {
+				["max-age"] = "3000";
+			}
+		}, pkp_patt:match([[max-age=3000; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="]]))
+
+		-- max-age is compulsory
+		assert.same(nil, pkp_patt:match([[pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="]]))
+	end)
+	it("Parses a HPKP Report header", function()
+		-- Example from RFC 7469 2.1.5
+		local pkp_patt = lpeg.Ct(http_pkp.Public_Key_Pins_Report_Only) * EOF
+		assert.same({
+			{
+				sha256 = {
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
+				};
+			}, {
+				["max-age"] = "3000";
+			}
+		}, pkp_patt:match([[max-age=3000; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="]]))
+		-- max-age isn't compulsory
+		assert.same({
+			{
+				sha256 = {
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
+				};
+			}, {
+			}
+		}, pkp_patt:match([[pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="]]))
+	end)
+end)
diff --git a/spec/http_slug_spec.lua b/spec/http_slug_spec.lua
new file mode 100644
index 0000000..0d48792
--- /dev/null
+++ b/spec/http_slug_spec.lua
@@ -0,0 +1,12 @@
+describe("lpeg_patterns.http.slug", function()
+	local http_slug = require "lpeg_patterns.http.slug"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a SLUG header", function()
+		local SLUG = http_slug.SLUG * EOF
+		assert.same("foo", SLUG:match("foo"))
+		assert.same("foo bar", SLUG:match("foo bar"))
+		assert.same("foo bar", SLUG:match("foo  bar"))
+		assert.same("foo   bar", SLUG:match("foo %20 bar"))
+	end)
+end)
diff --git a/spec/http_spec.lua b/spec/http_spec.lua
index 7cdbfa9..865a1e5 100644
--- a/spec/http_spec.lua
+++ b/spec/http_spec.lua
@@ -2,26 +2,6 @@ describe("http patterns", function()
 	local http = require "lpeg_patterns.http"
 	local lpeg = require "lpeg"
 	local EOF = lpeg.P(-1)
-	it("Parses a SLUG header", function()
-		local SLUG = http.SLUG * EOF
-		assert.same("foo", SLUG:match("foo"))
-		assert.same("foo bar", SLUG:match("foo bar"))
-		assert.same("foo bar", SLUG:match("foo  bar"))
-		assert.same("foo   bar", SLUG:match("foo %20 bar"))
-	end)
-	it("Parses an Origin header", function()
-		local Origin = lpeg.Ct(http.Origin) * EOF
-		assert.same({}, Origin:match("null"))
-		assert.same({"http://example.com"}, Origin:match("http://example.com"))
-		assert.same({"http://example.com", "https://foo.org"}, Origin:match("http://example.com https://foo.org"))
-	end)
-	it("Parses an X-Frame-Options header", function()
-		local X_Frame_Options = lpeg.Ct(http.X_Frame_Options) * EOF
-		assert.same({"deny"}, X_Frame_Options:match("deny"))
-		assert.same({"deny"}, X_Frame_Options:match("DENY"))
-		assert.same({"deny"}, X_Frame_Options:match("dEnY"))
-		assert.same({"http://example.com"}, X_Frame_Options:match("Allow-From http://example.com"))
-	end)
 	it("Splits a request line", function()
 		local request_line = lpeg.Ct(http.request_line) * EOF
 		assert.same({"GET", "/", 1.0}, request_line:match("GET / HTTP/1.0\r\n"))
@@ -86,7 +66,8 @@ describe("http patterns", function()
 		assert.same({{"foo", someext = "bar", another = "qux"}}, Transfer_Encoding:match("foo;someext=bar;another=\"qux\""))
 		-- q not allowed
 		assert.falsy(Transfer_Encoding:match("foo;q=0.5"))
-		assert.same({{"foo", queen = "foo"}}, Transfer_Encoding:match("foo;queen=foo")) -- check transfer parameters starting with q (but not q) are allowed
+		-- check transfer parameters starting with q (but not q) are allowed
+		assert.same({{"foo", queen = "foo"}}, Transfer_Encoding:match("foo;queen=foo"))
 	end)
 	it("Parses a TE header", function()
 		local TE = lpeg.Ct(http.TE) * EOF
@@ -113,13 +94,19 @@ describe("http patterns", function()
 	end)
 	it("Parses a Content-Type header", function()
 		local Content_Type = http.Content_Type * EOF
-		assert.same({ type = "foo", subtype = "bar", parameters = {}}, Content_Type:match("foo/bar"))
-		assert.same({ type = "foo", subtype = "bar", parameters = {param="value"}}, Content_Type:match("foo/bar;param=value"))
+		assert.same({ type = "foo", subtype = "bar", parameters = {}},
+			Content_Type:match("foo/bar"))
+		assert.same({ type = "foo", subtype = "bar", parameters = {param="value"}},
+			Content_Type:match("foo/bar;param=value"))
 		-- Examples from RFC7231 3.1.1.1.
-		assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}}, Content_Type:match([[text/html;charset=utf-8]]))
-		-- assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}}, Content_Type:match([[text/html;charset=UTF-8]]))
-		assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}}, Content_Type:match([[Text/HTML;Charset="utf-8"]]))
-		assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}}, Content_Type:match([[text/html; charset="utf-8"]]))
+		assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}},
+			Content_Type:match([[text/html;charset=utf-8]]))
+		-- assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}},
+		-- 	Content_Type:match([[text/html;charset=UTF-8]]))
+		assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}},
+			Content_Type:match([[Text/HTML;Charset="utf-8"]]))
+		assert.same({ type = "text", subtype = "html", parameters = {charset="utf-8"}},
+			Content_Type:match([[text/html; charset="utf-8"]]))
 	end)
 	it("Parses an Accept header", function()
 		local Accept = lpeg.Ct(http.Accept) * EOF
@@ -163,78 +150,15 @@ describe("http patterns", function()
 		assert.same(example_time, Date:match"Sunday, 06-Nov-94 08:49:37 GMT")
 		assert.same(example_time, Date:match"Sun Nov  6 08:49:37 1994")
 	end)
-	it("Parses a Sec-WebSocket-Extensions header", function()
-		local Sec_WebSocket_Extensions = lpeg.Ct(http.Sec_WebSocket_Extensions) * EOF
-		assert.same({{"foo", parameters = {}}}, Sec_WebSocket_Extensions:match"foo")
-		assert.same({{"foo", parameters = {}}, {"bar", parameters = {}}}, Sec_WebSocket_Extensions:match"foo, bar")
-		assert.same({{"foo", parameters = {hello = true; world = "extension"}}, {"bar", parameters = {}}}, Sec_WebSocket_Extensions:match"foo;hello;world=extension, bar")
-		assert.same({{"foo", parameters = {hello = true; world = "extension"}}, {"bar", parameters = {}}}, Sec_WebSocket_Extensions:match"foo;hello;world=\"extension\", bar")
-		-- quoted strings must be valid tokens
-		assert.falsy(Sec_WebSocket_Extensions:match"foo;hello;world=\"exte\\\"nsion\", bar")
-	end)
-	it("Parses a Sec_WebSocket-Version-Client header", function()
-		local Sec_WebSocket_Version_Client = http.Sec_WebSocket_Version_Client * EOF
-		assert.same(1, Sec_WebSocket_Version_Client:match"1")
-		assert.same(100, Sec_WebSocket_Version_Client:match"100")
-		assert.same(255, Sec_WebSocket_Version_Client:match"255")
-		assert.falsy(Sec_WebSocket_Version_Client:match"0")
-		assert.falsy(Sec_WebSocket_Version_Client:match"256")
-		assert.falsy(Sec_WebSocket_Version_Client:match"1.2")
-		assert.falsy(Sec_WebSocket_Version_Client:match"090")
-	end)
-	it("Parses a Link header", function()
-		local Link = lpeg.Ct(http.Link) * EOF
-		assert.same({{{host="example.com"}}}, Link:match"<//example.com>")
-		assert.same({{{scheme = "http"; host = "example.com"; path = "/TheBook/chapter2";}; rel = "previous"; title="previous chapter"}},
-			Link:match[[<http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"]])
-		assert.same({{{path = "/"}, rel = "http://example.net/foo"}},
-			Link:match[[</>; rel="http://example.net/foo"]])
-		assert.same({
-				{{path = "/TheBook/chapter2"}, rel = "previous", title = "letztes Kapitel"};
-				{{path = "/TheBook/chapter4"}, rel = "next", title = "nächstes Kapitel"};
-			},
-			Link:match[[</TheBook/chapter2>; rel="previous"; title*=UTF-8'de'letztes%20Kapitel, </TheBook/chapter4>; rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel]])
-		assert.same({{{scheme = "http"; host = "example.org"; path = "/"}, rel = "start http://example.net/relation/other"}},
-			Link:match[[<http://example.org/>; rel="start http://example.net/relation/other"]])
-	end)
-	it("Parses a Set-Cookie header", function()
-		local Set_Cookie = lpeg.Ct(http.Set_Cookie) * EOF
-		assert.same({"SID", "31d4d96e407aad42", {}}, Set_Cookie:match"SID=31d4d96e407aad42")
-		assert.same({"SID", "", {}}, Set_Cookie:match"SID=")
-		assert.same({"SID", "31d4d96e407aad42", {path="/"; domain="example.com"}}, Set_Cookie:match"SID=31d4d96e407aad42; Path=/; Domain=example.com")
-		assert.same({"SID", "31d4d96e407aad42", {
-			path = "/";
-			domain = "example.com";
-			secure = true;
-			expires = "Sun Nov  6 08:49:37 1994";
-		}}, Set_Cookie:match"SID=31d4d96e407aad42; Path=/; Domain=example.com; Secure; Expires=Sun Nov  6 08:49:37 1994")
-		-- Space before '='
-		assert.same({"SID", "31d4d96e407aad42", {path = "/";}}, Set_Cookie:match"SID=31d4d96e407aad42; Path =/")
-		-- Quoted cookie value
-		assert.same({"SID", "31d4d96e407aad42", {path = "/";}}, Set_Cookie:match[[SID="31d4d96e407aad42"; Path=/]])
-		-- Crazy whitespace
-		assert.same({"SID", "31d4d96e407aad42", {path = "/";}}, Set_Cookie:match"SID  =   31d4d96e407aad42  ;   Path  =  /")
-		assert.same({"SID", "31d4d96e407aad42", {["foo  bar"] = true;}}, Set_Cookie:match"SID  =   31d4d96e407aad42  ;  foo  bar")
-	end)
-	it("Parses a Cookie header", function()
-		local Cookie = http.Cookie * EOF
-		assert.same({SID = "31d4d96e407aad42"}, Cookie:match"SID=31d4d96e407aad42")
-		assert.same({SID = "31d4d96e407aad42"}, Cookie:match"SID = 31d4d96e407aad42")
-		assert.same({SID = "31d4d96e407aad42", lang = "en-US"}, Cookie:match"SID=31d4d96e407aad42; lang=en-US")
-	end)
-	it("Parses a Content-Disposition header", function()
-		local Content_Disposition = lpeg.Ct(http.Content_Disposition) * EOF
-		assert.same({"foo", {}}, Content_Disposition:match"foo")
-		assert.same({"foo", {filename="example"}}, Content_Disposition:match"foo; filename=example")
-		assert.same({"foo", {filename="example"}}, Content_Disposition:match"foo; filename*=UTF-8''example")
-	end)
-	it("Parses a Strict-Transport-Security header", function()
-		local sts_patt = lpeg.Cf(lpeg.Ct(true) * http.Strict_Transport_Security, rawset) * EOF
-		assert.same({["max-age"] = "0"}, sts_patt:match("max-age=0"))
-		assert.same({["max-age"] = "0"}, sts_patt:match("max-age = 0"))
-		assert.same({["max-age"] = "0"}, sts_patt:match("Max-Age=0"))
-		assert.same({["max-age"] = "0"; includesubdomains = true}, sts_patt:match("max-age=0;includeSubdomains"))
-		assert.same({["max-age"] = "0"; includesubdomains = true}, sts_patt:match("max-age=0 ; includeSubdomains"))
+	it("Parses a Cache-Control header", function()
+		local cc_patt = lpeg.Cf(lpeg.Ct(true) * http.Cache_Control, rawset) * EOF
+		assert.same({public = true}, cc_patt:match("public"))
+		assert.same({["no-cache"] = true}, cc_patt:match("no-cache"))
+		assert.same({["max-age"] = "31536000"}, cc_patt:match("max-age=31536000"))
+		assert.same({["max-age"] = "31536000", immutable = true}, cc_patt:match("max-age=31536000, immutable"))
+		-- leading/trailing whitespace
+		assert.same({public = true}, cc_patt:match("  public  "))
+		assert.same({["max-age"] = "31536000", immutable = true}, cc_patt:match("   max-age=31536000    ,    immutable   "))
 	end)
 	it("Parses an WWW_Authenticate header", function()
 		local WWW_Authenticate = lpeg.Ct(http.WWW_Authenticate) * EOF
@@ -245,13 +169,4 @@ describe("http patterns", function()
 		assert.same({{"Newauth", {realm = "apps", type="1", title="Login to \"apps\""}}, {"Basic", {realm="simple"}}},
 			WWW_Authenticate:match[[Newauth realm="apps", type=1, title="Login to \"apps\"", Basic realm="simple"]])
 	end)
-	it("Parses a HPKP header", function()
-		-- Example from RFC 7469 2.1.5
-		local pkp_patt = lpeg.Cf(lpeg.Ct(true) * http.Public_Key_Pins, function(t, k, v) table.insert(t, {k,v}) return t end) * EOF
-		assert.same({
-			{ "max-age", "3000" };
-			{ "pin-sha256", "d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" };
-			{ "pin-sha256", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" };
-		}, pkp_patt:match([[max-age=3000; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="]]))
-	end)
 end)
diff --git a/spec/http_sts_spec.lua b/spec/http_sts_spec.lua
new file mode 100644
index 0000000..77719a6
--- /dev/null
+++ b/spec/http_sts_spec.lua
@@ -0,0 +1,17 @@
+describe("lpeg_patterns.http.sts", function()
+	local http_sts = require "lpeg_patterns.http.sts"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a Strict-Transport-Security header", function()
+		local sts_patt = http_sts.Strict_Transport_Security * EOF
+		assert.same({["max-age"] = "0"}, sts_patt:match("max-age=0"))
+		assert.same({["max-age"] = "0"}, sts_patt:match("max-age = 0"))
+		assert.same({["max-age"] = "0"}, sts_patt:match("Max-Age=0"))
+		assert.same({["max-age"] = "0"; includesubdomains = true}, sts_patt:match("max-age=0;includeSubdomains"))
+		assert.same({["max-age"] = "0"; includesubdomains = true}, sts_patt:match("max-age=0 ; includeSubdomains"))
+		-- max-age is required
+		assert.same(nil, sts_patt:match("foo=0"))
+		-- Should fail to parse when duplicate field given.
+		assert.same(nil, sts_patt:match("max-age=42; foo=0; foo=1"))
+	end)
+end)
diff --git a/spec/http_websocket_spec.lua b/spec/http_websocket_spec.lua
new file mode 100644
index 0000000..ed88353
--- /dev/null
+++ b/spec/http_websocket_spec.lua
@@ -0,0 +1,28 @@
+describe("lpeg_patterns.http.websocket", function()
+	local http_websocket = require "lpeg_patterns.http.websocket"
+	local lpeg = require "lpeg"
+	local EOF = lpeg.P(-1)
+	it("Parses a Sec-WebSocket-Extensions header", function()
+		local Sec_WebSocket_Extensions = lpeg.Ct(http_websocket.Sec_WebSocket_Extensions) * EOF
+		assert.same({{"foo", parameters = {}}},
+			Sec_WebSocket_Extensions:match"foo")
+		assert.same({{"foo", parameters = {}}, {"bar", parameters = {}}},
+			Sec_WebSocket_Extensions:match"foo, bar")
+		assert.same({{"foo", parameters = {hello = true; world = "extension"}}, {"bar", parameters = {}}},
+			Sec_WebSocket_Extensions:match"foo;hello;world=extension, bar")
+		assert.same({{"foo", parameters = {hello = true; world = "extension"}}, {"bar", parameters = {}}},
+			Sec_WebSocket_Extensions:match"foo;hello;world=\"extension\", bar")
+		-- quoted strings must be valid tokens
+		assert.falsy(Sec_WebSocket_Extensions:match"foo;hello;world=\"exte\\\"nsion\", bar")
+	end)
+	it("Parses a Sec_WebSocket-Version-Client header", function()
+		local Sec_WebSocket_Version_Client = http_websocket.Sec_WebSocket_Version_Client * EOF
+		assert.same(1, Sec_WebSocket_Version_Client:match"1")
+		assert.same(100, Sec_WebSocket_Version_Client:match"100")
+		assert.same(255, Sec_WebSocket_Version_Client:match"255")
+		assert.falsy(Sec_WebSocket_Version_Client:match"0")
+		assert.falsy(Sec_WebSocket_Version_Client:match"256")
+		assert.falsy(Sec_WebSocket_Version_Client:match"1.2")
+		assert.falsy(Sec_WebSocket_Version_Client:match"090")
+	end)
+end)
diff --git a/spec/language_spec.lua b/spec/language_spec.lua
index cc0bc7c..dfd418c 100644
--- a/spec/language_spec.lua
+++ b/spec/language_spec.lua
@@ -6,64 +6,123 @@ describe("language tags", function()
 	local Language_Tag = language.Language_Tag * EOF
 	describe("examples from RFC 5646 Appendix A", function()
 		it("Parses Simple language subtag", function()
-			assert.same({language = "de"}, langtag:match "de") -- German
-			assert.same({language = "fr"}, langtag:match "fr") -- French
-			assert.same({language = "ja"}, langtag:match "ja") -- Japanese
-			assert.truthy(Language_Tag:match "i-enochian") -- example of a grandfathered tag
+			-- German
+			assert.same({language = "de"}, langtag:match "de")
+			-- French
+			assert.same({language = "fr"}, langtag:match "fr")
+			-- Japanese
+			assert.same({language = "ja"}, langtag:match "ja")
+			-- example of a grandfathered tag
+			assert.truthy(Language_Tag:match "i-enochian")
 		end)
 		it("Parses Language subtag plus Script subtag", function()
-			assert.same({language = "zh"; script = "Hant"}, langtag:match "zh-Hant") -- Chinese written using the Traditional Chinese script
-			assert.same({language = "zh"; script = "Hans"}, langtag:match "zh-Hans") -- Chinese written using the Simplified Chinese script
-			assert.same({language = "sr"; script = "Cyrl"}, langtag:match "sr-Cyrl") -- Serbian written using the Cyrillic script
-			assert.same({language = "sr"; script = "Latn"}, langtag:match "sr-Latn") -- Serbian written using the Latin script
+			-- Chinese written using the Traditional Chinese script
+			assert.same({language = "zh"; script = "Hant"},
+				langtag:match "zh-Hant")
+			-- Chinese written using the Simplified Chinese script
+			assert.same({language = "zh"; script = "Hans"},
+				langtag:match "zh-Hans")
+			-- Serbian written using the Cyrillic script
+			assert.same({language = "sr"; script = "Cyrl"},
+				langtag:match "sr-Cyrl")
+			-- Serbian written using the Latin script
+			assert.same({language = "sr"; script = "Latn"},
+				langtag:match "sr-Latn")
 		end)
 		it("Parses Extended language subtags and their primary language subtag counterparts", function()
-			assert.same({language = "zh"; extlang = "cmn", script = "Hans"; region = "CN"}, langtag:match "zh-cmn-Hans-CN") -- Chinese, Mandarin, Simplified script, as used in China
-			assert.same({language = "cmn"; script = "Hans"; region = "CN"}, langtag:match "cmn-Hans-CN") -- Mandarin Chinese, Simplified script, as used in China
-			assert.same({language = "zh"; extlang = "yue"; region = "HK"}, langtag:match "zh-yue-HK") -- Chinese, Cantonese, as used in Hong Kong SAR
-			assert.same({language = "yue"; region = "HK"}, langtag:match "yue-HK") -- Cantonese Chinese, as used in Hong Kong SAR
+			-- Chinese, Mandarin, Simplified script, as used in China
+			assert.same({language = "zh"; extlang = "cmn", script = "Hans"; region = "CN"},
+				langtag:match "zh-cmn-Hans-CN")
+			-- Mandarin Chinese, Simplified script, as used in China
+			assert.same({language = "cmn"; script = "Hans"; region = "CN"},
+				langtag:match "cmn-Hans-CN")
+			-- Chinese, Cantonese, as used in Hong Kong SAR
+			assert.same({language = "zh"; extlang = "yue"; region = "HK"},
+				langtag:match "zh-yue-HK")
+			-- Cantonese Chinese, as used in Hong Kong SAR
+			assert.same({language = "yue"; region = "HK"},
+				langtag:match "yue-HK")
 		end)
 		it("Parses Language-Script-Region", function()
-			assert.same({language = "zh"; script = "Hans"; region = "CN"}, langtag:match "zh-Hans-CN") -- Chinese written using the Simplified script as used in mainland China
-			assert.same({language = "sr"; script = "Latn"; region = "RS"}, langtag:match "sr-Latn-RS") -- Serbian written using the Latin script as used in Serbia
+			-- Chinese written using the Simplified script as used in mainland China
+			assert.same({language = "zh"; script = "Hans"; region = "CN"},
+				langtag:match "zh-Hans-CN")
+			-- Serbian written using the Latin script as used in Serbia
+			assert.same({language = "sr"; script = "Latn"; region = "RS"},
+				langtag:match "sr-Latn-RS")
 		end)
 		it("Parses Language-Variant", function()
-			assert.same({language = "sl"; variant = {"rozaj"}}, langtag:match "sl-rozaj") -- Resian dialect of Slovenian
-			assert.same({language = "sl"; variant = {"rozaj", "biske"}}, langtag:match "sl-rozaj-biske") -- San Giorgio dialect of Resian dialect of Slovenian
-			assert.same({language = "sl"; variant = {"nedis"}}, langtag:match "sl-nedis") -- Nadiza dialect of Slovenian
+			-- Resian dialect of Slovenian
+			assert.same({language = "sl"; variant = {"rozaj"}},
+				langtag:match "sl-rozaj")
+			-- San Giorgio dialect of Resian dialect of Slovenian
+			assert.same({language = "sl"; variant = {"rozaj", "biske"}},
+				langtag:match "sl-rozaj-biske")
+			-- Nadiza dialect of Slovenian
+			assert.same({language = "sl"; variant = {"nedis"}},
+				langtag:match "sl-nedis")
 		end)
 		it("Parses Language-Region-Variant", function()
-			assert.same({language = "de"; region = "CH"; variant = {"1901"}}, langtag:match "de-CH-1901") -- German as used in Switzerland using the 1901 variant [orthography]
-			assert.same({language = "sl"; region = "IT"; variant = {"nedis"}}, langtag:match "sl-IT-nedis") -- Slovenian as used in Italy, Nadiza dialect
+			-- German as used in Switzerland using the 1901 variant [orthography]
+			assert.same({language = "de"; region = "CH"; variant = {"1901"}},
+				langtag:match "de-CH-1901")
+			-- Slovenian as used in Italy, Nadiza dialect
+			assert.same({language = "sl"; region = "IT"; variant = {"nedis"}},
+				langtag:match "sl-IT-nedis")
 		end)
 		it("Parses Language-Script-Region-Variant", function()
-			assert.same({language = "hy"; script = "Latn"; region = "IT"; variant = {"arevela"}}, langtag:match "hy-Latn-IT-arevela") -- Eastern Armenian written in Latin script, as used in Italy
+			-- Eastern Armenian written in Latin script, as used in Italy
+			assert.same({language = "hy"; script = "Latn"; region = "IT"; variant = {"arevela"}},
+				langtag:match "hy-Latn-IT-arevela")
 		end)
 		it("Parses Language-Region", function()
-			assert.same({language = "de"; region = "DE"}, langtag:match "de-DE") -- German for Germany
-			assert.same({language = "en"; region = "US"}, langtag:match "en-US") -- English as used in the United States
-			assert.same({language = "es"; region = "419"}, langtag:match "es-419") -- Spanish appropriate for the Latin America and Caribbean region using the UN region code
+			-- German for Germany
+			assert.same({language = "de"; region = "DE"},
+				langtag:match "de-DE")
+			-- English as used in the United States
+			assert.same({language = "en"; region = "US"},
+				langtag:match "en-US")
+			-- Spanish appropriate for the Latin America and Caribbean region using the UN region code
+			assert.same({language = "es"; region = "419"},
+				langtag:match "es-419")
 		end)
 		it("Parses private use subtags", function()
-			assert.same({language = "de"; region = "CH"; privateuse = {"phonebk"}}, langtag:match "de-CH-x-phonebk")
-			assert.same({language = "az"; script = "Arab"; privateuse = {"AZE", "derbend"}}, langtag:match "az-Arab-x-AZE-derbend")
+			assert.same({language = "de"; region = "CH"; privateuse = {"phonebk"}},
+				langtag:match "de-CH-x-phonebk")
+			assert.same({language = "az"; script = "Arab"; privateuse = {"AZE", "derbend"}},
+				langtag:match "az-Arab-x-AZE-derbend")
 		end)
 		it("Parses private use registry values", function()
 			assert.truthy(Language_Tag:match "x-whatever") -- private use using the singleton 'x'
-			assert.same({language = "qaa"; script = "Qaaa"; region = "QM"; privateuse = {"southern"}}, langtag:match "qaa-Qaaa-QM-x-southern") -- all private tags
-			assert.same({language = "de"; script = "Qaaa"}, langtag:match "de-Qaaa") -- German, with a private script
-			assert.same({language = "sr"; script = "Latn"; region = "QM"}, langtag:match "sr-Latn-QM") -- Serbian, Latin script, private region
-			assert.same({language = "sr"; script = "Qaaa"; region = "RS"}, langtag:match "sr-Qaaa-RS") -- Serbian, private script, for Serbia
+			-- all private tags
+			assert.same({language = "qaa"; script = "Qaaa"; region = "QM"; privateuse = {"southern"}},
+				langtag:match "qaa-Qaaa-QM-x-southern")
+			-- German, with a private script
+			assert.same({language = "de"; script = "Qaaa"},
+				langtag:match "de-Qaaa")
+			-- Serbian, Latin script, private region
+			assert.same({language = "sr"; script = "Latn"; region = "QM"},
+				langtag:match "sr-Latn-QM")
+			-- Serbian, private script, for Serbia
+			assert.same({language = "sr"; script = "Qaaa"; region = "RS"},
+				langtag:match "sr-Qaaa-RS")
 		end)
 		it("Parses tags that use extensions", function()
-			assert.same({language = "en"; region = "US"; extension = { u = {"islamcal"}}}, langtag:match "en-US-u-islamcal")
-			assert.same({language = "zh"; region = "CN"; extension = { a = {"myext"}}; privateuse = {"private"}}, langtag:match "zh-CN-a-myext-x-private")
-			assert.same({language = "en"; extension = { a = {"myext"}, b = {"another"}}}, langtag:match "en-a-myext-b-another")
+			assert.same({language = "en"; region = "US"; extension = { u = {"islamcal"}}},
+				langtag:match "en-US-u-islamcal")
+			assert.same({language = "zh"; region = "CN"; extension = { a = {"myext"}}; privateuse = {"private"}},
+				langtag:match "zh-CN-a-myext-x-private")
+			assert.same({language = "en"; extension = { a = {"myext"}, b = {"another"}}},
+				langtag:match "en-a-myext-b-another")
 		end)
 		it("Rejects Invalid Tags", function()
-			assert.falsy(langtag:match "de-419-DE") -- two region tags
-			assert.falsy(langtag:match "a-DE") -- use of a single-character subtag in primary position; note that there are a few grandfathered tags that start with "i-" that are valid
-			assert.falsy(langtag:match "ar-a-aaa-b-bbb-a-ccc") -- two extensions with same single-letter prefix
+			-- two region tags
+			assert.falsy(langtag:match "de-419-DE")
+			-- use of a single-character subtag in primary position;
+			-- note that there are a few grandfathered tags that start with "i-" that are valid
+			assert.falsy(langtag:match "a-DE")
+			-- two extensions with same single-letter prefix
+			assert.falsy(langtag:match "ar-a-aaa-b-bbb-a-ccc")
 		end)
 	end)
 	it("captures whole text when using Language_Tag", function()
diff --git a/spec/uri_spec.lua b/spec/uri_spec.lua
index 1f0144a..cbb7672 100644
--- a/spec/uri_spec.lua
+++ b/spec/uri_spec.lua
@@ -102,7 +102,8 @@ describe("URI", function()
 		assert.same({scheme="scheme", host="example.com", path="/"}, ref:match "scheme://example.com/")
 	end)
 	it("Should work with mailto URIs", function()
-		assert.same({scheme="mailto", path="user@example.com"}, uri:match "mailto:user@example.com")
+		assert.same({scheme="mailto", path="user@example.com"},
+			uri:match "mailto:user@example.com")
 		assert.same({scheme="mailto", path="someone@example.com,someoneelse@example.com"},
 			uri:match "mailto:someone@example.com,someoneelse@example.com")
 		assert.same({scheme="mailto", path="user@example.com", query="subject=This%20is%20the%20subject&cc=someone_else@example.com&body=This%20is%20the%20body"},
@@ -110,7 +111,8 @@ describe("URI", function()
 
 		-- Examples from RFC-6068
 		-- Section 6.1
-		assert.same({scheme="mailto", path="chris@example.com"}, uri:match "mailto:chris@example.com")
+		assert.same({scheme="mailto", path="chris@example.com"},
+			uri:match "mailto:chris@example.com")
 		assert.same({scheme="mailto", path="infobot@example.com", query="subject=current-issue"},
 			uri:match "mailto:infobot@example.com?subject=current-issue")
 		assert.same({scheme="mailto", path="infobot@example.com", query="body=send%20current-issue"},
@@ -123,35 +125,47 @@ describe("URI", function()
 			uri:match "mailto:majordomo@example.com?body=subscribe%20bamboo-l")
 		assert.same({scheme="mailto", path="joe@example.com", query="cc=bob@example.com&body=hello"},
 			uri:match "mailto:joe@example.com?cc=bob@example.com&body=hello")
-		assert.same({scheme="mailto", path="gorby%25kremvax@example.com"}, uri:match "mailto:gorby%25kremvax@example.com")
+		assert.same({scheme="mailto", path="gorby%25kremvax@example.com"},
+			uri:match "mailto:gorby%25kremvax@example.com")
 		assert.same({scheme="mailto", path="unlikely%3Faddress@example.com", query="blat=foop"},
 			uri:match "mailto:unlikely%3Faddress@example.com?blat=foop")
-		assert.same({scheme="mailto", path="Mike%26family@example.org"}, uri:match "mailto:Mike%26family@example.org")
+		assert.same({scheme="mailto", path="Mike%26family@example.org"},
+			uri:match "mailto:Mike%26family@example.org")
 		-- Section 6.2
-		assert.same({scheme="mailto", path=[[%22not%40me%22@example.org]]}, uri:match "mailto:%22not%40me%22@example.org")
-		assert.same({scheme="mailto", path=[[%22oh%5C%5Cno%22@example.org]]}, uri:match "mailto:%22oh%5C%5Cno%22@example.org")
+		assert.same({scheme="mailto", path=[[%22not%40me%22@example.org]]},
+			uri:match "mailto:%22not%40me%22@example.org")
+		assert.same({scheme="mailto", path=[[%22oh%5C%5Cno%22@example.org]]},
+			uri:match "mailto:%22oh%5C%5Cno%22@example.org")
 		assert.same({scheme="mailto", path=[[%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org]]},
 			uri:match "mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org")
 	end)
 	it("Should work with xmpp URIs", function()
 		-- Examples from RFC-5122
-		assert.same({scheme="xmpp", path="node@example.com"}, uri:match "xmpp:node@example.com")
-		assert.same({scheme="xmpp", userinfo="guest", host="example.com"}, uri:match "xmpp://guest@example.com")
+		assert.same({scheme="xmpp", path="node@example.com"},
+			uri:match "xmpp:node@example.com")
+		assert.same({scheme="xmpp", userinfo="guest", host="example.com"},
+			uri:match "xmpp://guest@example.com")
 		assert.same({scheme="xmpp", userinfo="guest", host="example.com", path="/support@example.com", query="message"},
 			uri:match "xmpp://guest@example.com/support@example.com?message")
-		assert.same({scheme="xmpp", path="support@example.com", query="message"}, uri:match "xmpp:support@example.com?message")
+		assert.same({scheme="xmpp", path="support@example.com", query="message"},
+			uri:match "xmpp:support@example.com?message")
 
-		assert.same({scheme="xmpp", path="example-node@example.com"}, uri:match "xmpp:example-node@example.com")
-		assert.same({scheme="xmpp", path="example-node@example.com/some-resource"}, uri:match "xmpp:example-node@example.com/some-resource")
-		assert.same({scheme="xmpp", path="example.com"}, uri:match "xmpp:example.com")
-		assert.same({scheme="xmpp", path="example-node@example.com", query="message"}, uri:match "xmpp:example-node@example.com?message")
+		assert.same({scheme="xmpp", path="example-node@example.com"},
+			uri:match "xmpp:example-node@example.com")
+		assert.same({scheme="xmpp", path="example-node@example.com/some-resource"},
+			uri:match "xmpp:example-node@example.com/some-resource")
+		assert.same({scheme="xmpp", path="example.com"},
+			uri:match "xmpp:example.com")
+		assert.same({scheme="xmpp", path="example-node@example.com", query="message"},
+			uri:match "xmpp:example-node@example.com?message")
 		assert.same({scheme="xmpp", path="example-node@example.com", query="message;subject=Hello%20World"},
 			uri:match "xmpp:example-node@example.com?message;subject=Hello%20World")
 		assert.same({scheme="xmpp", path=[[nasty!%23$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com]]},
 			uri:match "xmpp:nasty!%23$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com")
 		assert.same({scheme="xmpp", path=[[node@example.com/repulsive%20!%23%22$%25&'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource]]},
 			uri:match [[xmpp:node@example.com/repulsive%20!%23%22$%25&'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource]])
-		assert.same({scheme="xmpp", path="ji%C5%99i@%C4%8Dechy.example/v%20Praze"}, uri:match "xmpp:ji%C5%99i@%C4%8Dechy.example/v%20Praze")
+		assert.same({scheme="xmpp", path="ji%C5%99i@%C4%8Dechy.example/v%20Praze"},
+			uri:match "xmpp:ji%C5%99i@%C4%8Dechy.example/v%20Praze")
 	end)
 end)