File: http_spec.lua

package info (click to toggle)
lua-lpeg-patterns 0.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 220 kB
  • sloc: makefile: 6
file content (334 lines) | stat: -rw-r--r-- 17,228 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
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"))
		assert.same({"GET", "http://foo.com/", 1.0}, request_line:match("GET http://foo.com/ HTTP/1.0\r\n"))
		assert.same({"OPTIONS", "*", 1.1}, request_line:match("OPTIONS * HTTP/1.1\r\n"))
	end)
	it("Splits an Upgrade header", function()
		local Upgrade = lpeg.Ct(http.Upgrade) * EOF
		assert.same({"Foo"}, Upgrade:match("Foo"))
		assert.same({"WebSocket"}, Upgrade:match("WebSocket"))
		assert.same({"HTTP/2.0", "SHTTP/1.3", "IRC/6.9", "RTA/x11"}, Upgrade:match("HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11"))
	end)
	it("Splits a Via header", function()
		local Via = lpeg.Ct(http.Via) * EOF
		assert.same({{protocol="HTTP/1.0", by="fred"}}, Via:match("1.0 fred"))
		assert.same({{protocol="HTTP/1.0", by="fred"}}, Via:match("HTTP/1.0 fred"))
		assert.same({{protocol="Other/myversion", by="fred"}}, Via:match("Other/myversion fred"))
		assert.same({{protocol="HTTP/1.1", by="p.example.net"}}, Via:match("1.1 p.example.net"))
		assert.same({
			{protocol="HTTP/1.0", by="fred"},
			{protocol="HTTP/1.1", by="p.example.net"}
		}, Via:match("1.0 fred, 1.1 p.example.net"))
		assert.same({
			{protocol="HTTP/1.0", by="my.host:80"},
			{protocol="HTTP/1.1", by="my.other.host"}
		}, Via:match("1.0 my.host:80, 1.1 my.other.host"))
		assert.same({
			{protocol="HTTP/1.0", by="fred"},
			{protocol="HTTP/1.1", by="p.example.net"}
		}, Via:match(",,,1.0 fred ,  ,,, 1.1 p.example.net,,,"))
	end)
	it("Handles folding whitespace in field_value", function()
		local field_value = http.field_value * EOF
		assert.same("Foo", field_value:match("Foo"))
		-- doesn't remove repeated whitespace
		assert.same("Foo  Bar", field_value:match("Foo  Bar"))
		-- unfolds whitespace broken over multiple lines
		assert.same("Foo Bar", field_value:match("Foo\r\n Bar"))
		assert.same("Foo Bar", field_value:match("Foo \r\n Bar"))
	end)
	it("Splits a Connection header", function()
		local Connection = lpeg.Ct(http.Connection) * EOF
		assert.same({}, Connection:match(" "))
		assert.same({}, Connection:match(","))
		assert.same({}, Connection:match(",    ,"))
		assert.same({"foo"}, Connection:match("foo"))
		assert.same({"foo"}, Connection:match(" foo"))
		assert.same({"foo"}, Connection:match(" foo,,,"))
		assert.same({"foo"}, Connection:match(",, , foo  "))
		assert.same({"foo", "bar"}, Connection:match("foo,bar"))
		assert.same({"foo", "bar"}, Connection:match("foo, bar"))
		assert.same({"foo", "bar"}, Connection:match("foo , bar"))
		assert.same({"foo", "bar"}, Connection:match("foo\t, bar"))
		assert.same({"foo", "bar"}, Connection:match("foo,,,  ,bar"))
	end)
	it("Parses a Transfer-Encoding header", function()
		local Transfer_Encoding = lpeg.Ct(http.Transfer_Encoding) * EOF
		assert.falsy(Transfer_Encoding:match("")) -- doesn't allow empty
		assert.same({{"foo"}}, Transfer_Encoding:match("foo"))
		assert.same({{"foo"}, {"bar"}}, Transfer_Encoding:match("foo, bar"))
		assert.same({{"foo", someext = "bar"}}, Transfer_Encoding:match("foo;someext=bar"))
		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"))
		-- 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
		assert.same({}, TE:match("")) -- allows empty
		assert.same({{"foo"}}, TE:match("foo"))
		assert.same({{"foo"}, {"bar"}}, TE:match("foo, bar"))
		assert.same({{"foo", q=0.5}}, TE:match("foo;q=0.5"))
		assert.same({{"foo", someext = "foo", q=0.5}}, TE:match("foo;someext=foo;q=0.5"))
	end)
	it("Splits a Trailer header", function()
		local Trailer = lpeg.Ct(http.Trailer) * EOF
		assert.falsy(Trailer:match(" "))
		assert.falsy(Trailer:match(","))
		assert.falsy(Trailer:match(",    ,"))
		assert.same({"foo"}, Trailer:match("foo"))
		assert.same({"foo"}, Trailer:match(" foo"))
		assert.same({"foo"}, Trailer:match(" foo,,,"))
		assert.same({"foo"}, Trailer:match(",, , foo  "))
		assert.same({"foo", "bar"}, Trailer:match("foo,bar"))
		assert.same({"foo", "bar"}, Trailer:match("foo, bar"))
		assert.same({"foo", "bar"}, Trailer:match("foo , bar"))
		assert.same({"foo", "bar"}, Trailer:match("foo\t, bar"))
		assert.same({"foo", "bar"}, Trailer:match("foo,,,  ,bar"))
	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"))
		-- 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"]]))
	end)
	it("Parses an Accept header", function()
		local Accept = lpeg.Ct(http.Accept) * EOF
		assert.same({{type = "foo", subtype = "bar", parameters = {}, q = nil, extensions = {}}}, Accept:match("foo/bar"))
		assert.same({
				{type = "audio", subtype = nil, parameters = {}, q = 0.2, extensions = {}};
				{type = "audio", subtype = "basic", parameters = {}, q = nil, extensions = {}};
			}, Accept:match("audio/*; q=0.2, audio/basic"))
		assert.same({
				{type = "text", subtype = "plain", parameters = {}, q = 0.5, extensions = {}};
				{type = "text", subtype = "html", parameters = {}, q = nil, extensions = {}};
				{type = "text", subtype = "x-dvi", parameters = {}, q = 0.8, extensions = {}};
				{type = "text", subtype = "x-c", parameters = {}, q = nil, extensions = {}};
			}, Accept:match("text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"))
		assert.same({
				{type = "text", subtype = nil, parameters = {}, extensions = {}};
				{type = "text", subtype = "plain", parameters = {}, extensions = {}};
				{type = "text", subtype = "plain", parameters = {format = "flowed"}, extensions = {}};
				{type = nil, subtype = nil, parameters = {}, extensions = {}};
			}, Accept:match("text/*, text/plain, text/plain;format=flowed, */*"))
		assert.same({
				{type = "text", subtype = nil, parameters = {}, q = 0.3, extensions = {}};
				{type = "text", subtype = "html", parameters = {}, q = 0.7, extensions = {}};
				{type = "text", subtype = "html", parameters = {level = "1"}, q = nil, extensions = {}};
				{type = "text", subtype = "html", parameters = {level = "2"}, q = 0.4, extensions = {}};
				{type = nil, subtype = nil, parameters = {}, q = 0.5, extensions = {}};
			}, Accept:match("text/*;q=0.3, text/html;q=0.7, text/html;level=1,text/html;level=2;q=0.4, */*;q=0.5"))
	end)
	it("Matches the 3 date formats", function()
		local Date = http.Date * EOF
		local example_time = {
			year = 1994;
			month = 11;
			day = 6;
			hour = 8;
			min = 49;
			sec = 37;
			wday = 1;
		}
		assert.same(example_time, Date:match"Sun, 06 Nov 1994 08:49:37 GMT")
		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 = http.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)
	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
		assert.same({{"Newauth"}}, WWW_Authenticate:match"Newauth")
		assert.same({{"Newauth", {realm = "apps"}}}, WWW_Authenticate:match[[Newauth realm="apps"]])
		assert.same({{"Newauth", {realm = "apps"}}}, WWW_Authenticate:match[[Newauth ReaLm="apps"]])
		assert.same({{"Newauth"}, {"Basic"}}, WWW_Authenticate:match"Newauth, Basic")
		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.Ct(http.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.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)
	it("Parses a Expect-Ct header", function()
		-- Examples from draft-ietf-httpbis-expect-ct-06 2.1.4
		local sts_patt = http.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)