http-security 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +21 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +17 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +20 -0
- data/README.md +90 -0
- data/Rakefile +34 -0
- data/http-security.gemspec +23 -0
- data/lib/http/security.rb +2 -0
- data/lib/http/security/exceptions.rb +8 -0
- data/lib/http/security/headers.rb +12 -0
- data/lib/http/security/headers/cache_control.rb +36 -0
- data/lib/http/security/headers/content_security_policy.rb +71 -0
- data/lib/http/security/headers/content_security_policy_report_only.rb +10 -0
- data/lib/http/security/headers/pragma.rb +24 -0
- data/lib/http/security/headers/public_key_pins.rb +60 -0
- data/lib/http/security/headers/public_key_pins_report_only.rb +10 -0
- data/lib/http/security/headers/set_cookie.rb +75 -0
- data/lib/http/security/headers/strict_transport_security.rb +29 -0
- data/lib/http/security/headers/x_content_type_options.rb +24 -0
- data/lib/http/security/headers/x_frame_options.rb +39 -0
- data/lib/http/security/headers/x_permitted_cross_domain_policies.rb +47 -0
- data/lib/http/security/headers/x_xss_protection.rb +34 -0
- data/lib/http/security/http_date.rb +13 -0
- data/lib/http/security/malformed_header.rb +33 -0
- data/lib/http/security/parsers.rb +14 -0
- data/lib/http/security/parsers/cache_control.rb +62 -0
- data/lib/http/security/parsers/content_security_policy.rb +128 -0
- data/lib/http/security/parsers/content_security_policy_report_only.rb +10 -0
- data/lib/http/security/parsers/expires.rb +19 -0
- data/lib/http/security/parsers/parser.rb +408 -0
- data/lib/http/security/parsers/pragma.rb +25 -0
- data/lib/http/security/parsers/public_key_pins.rb +43 -0
- data/lib/http/security/parsers/public_key_pins_report_only.rb +10 -0
- data/lib/http/security/parsers/set_cookie.rb +62 -0
- data/lib/http/security/parsers/strict_transport_security.rb +42 -0
- data/lib/http/security/parsers/x_content_type_options.rb +19 -0
- data/lib/http/security/parsers/x_frame_options.rb +47 -0
- data/lib/http/security/parsers/x_permitted_cross_domain_policies.rb +33 -0
- data/lib/http/security/parsers/x_xss_protection.rb +27 -0
- data/lib/http/security/response.rb +323 -0
- data/lib/http/security/version.rb +5 -0
- data/spec/data/alexa.csv +100 -0
- data/spec/headers/cache_control_spec.rb +40 -0
- data/spec/headers/content_security_policy_spec.rb +46 -0
- data/spec/headers/pragma_spec.rb +26 -0
- data/spec/headers/public_key_pins_spec.rb +68 -0
- data/spec/headers/set_cookie_spec.rb +122 -0
- data/spec/headers/strict_transport_security_spec.rb +39 -0
- data/spec/headers/x_content_type_options_spec.rb +26 -0
- data/spec/headers/x_frame_options_spec.rb +86 -0
- data/spec/headers/x_permitted_cross_domain_policies_spec.rb +108 -0
- data/spec/headers/x_xss_protection_spec.rb +59 -0
- data/spec/parsers/cache_control_spec.rb +26 -0
- data/spec/parsers/content_security_policy_report_only_spec.rb +48 -0
- data/spec/parsers/content_security_policy_spec.rb +74 -0
- data/spec/parsers/expires_spec.rb +71 -0
- data/spec/parsers/parser_spec.rb +317 -0
- data/spec/parsers/pragma_spec.rb +10 -0
- data/spec/parsers/public_key_pins_spec.rb +81 -0
- data/spec/parsers/set_cookie_spec.rb +55 -0
- data/spec/parsers/strict_transport_security_spec.rb +62 -0
- data/spec/parsers/x_content_type_options_spec.rb +10 -0
- data/spec/parsers/x_frame_options_spec.rb +24 -0
- data/spec/parsers/x_permitted_cross_domain_policies_spec.rb +34 -0
- data/spec/parsers/x_xss_protection_spec.rb +39 -0
- data/spec/response_spec.rb +262 -0
- data/spec/spec_helper.rb +13 -0
- data/tasks/alexa.rb +40 -0
- metadata +171 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
module Headers
|
4
|
+
class SetCookie
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# @return [Array<Cookie>]
|
9
|
+
attr_reader :cookies
|
10
|
+
|
11
|
+
class Cookie
|
12
|
+
|
13
|
+
attr_reader :cookie
|
14
|
+
|
15
|
+
attr_reader :path
|
16
|
+
|
17
|
+
attr_reader :domain
|
18
|
+
|
19
|
+
attr_reader :expires
|
20
|
+
|
21
|
+
def initialize(directives={})
|
22
|
+
@cookie = directives[:cookie]
|
23
|
+
@path = directives[:path]
|
24
|
+
@domain = directives[:domain]
|
25
|
+
@expires = directives[:expires]
|
26
|
+
@secure = directives[:secure]
|
27
|
+
@http_only = directives[:http_only]
|
28
|
+
end
|
29
|
+
|
30
|
+
def name
|
31
|
+
@cookie.keys.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def value
|
35
|
+
@cookie.values.first
|
36
|
+
end
|
37
|
+
|
38
|
+
def secure?
|
39
|
+
!!@secure
|
40
|
+
end
|
41
|
+
|
42
|
+
def http_only?
|
43
|
+
!!@http_only
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
str = "#{name}=#{value}"
|
48
|
+
|
49
|
+
str << "; Path=#{@path}" if @path
|
50
|
+
str << "; Domain=#{@domain}" if @domain
|
51
|
+
str << "; Expires=#{@expires.httpdate}" if @expires
|
52
|
+
str << "; Secure" if @secure
|
53
|
+
str << "; HttpOnly" if @http_only
|
54
|
+
|
55
|
+
return str
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(cookies=[])
|
61
|
+
@cookies = cookies.map { |cookie| Cookie.new(cookie) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def each(&block)
|
65
|
+
@cookies.each(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
@cookies.map(&:to_s).join(', ')
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
module Headers
|
4
|
+
class StrictTransportSecurity
|
5
|
+
|
6
|
+
attr_reader :max_age
|
7
|
+
|
8
|
+
def initialize(directives={})
|
9
|
+
@max_age = directives[:max_age]
|
10
|
+
@include_sub_domains = directives[:includesubdomains]
|
11
|
+
end
|
12
|
+
|
13
|
+
def include_sub_domains?
|
14
|
+
!!@include_sub_domains
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
directives = []
|
19
|
+
|
20
|
+
directives << "max-age=#{@max_age}" if @max_age
|
21
|
+
directives << "includeSubDomains" if @include_sub_domains
|
22
|
+
|
23
|
+
return directives.join('; ')
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
module Headers
|
4
|
+
class XContentTypeOptions
|
5
|
+
|
6
|
+
def initialize(directives={})
|
7
|
+
@no_sniff = directives[:nosniff]
|
8
|
+
end
|
9
|
+
|
10
|
+
def no_sniff?
|
11
|
+
!!@no_sniff
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
str = ''
|
16
|
+
str << "nosniff" if @no_sniff
|
17
|
+
|
18
|
+
return str
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
module Headers
|
4
|
+
class XFrameOptions
|
5
|
+
|
6
|
+
attr_reader :allow_from
|
7
|
+
|
8
|
+
def initialize(directives={})
|
9
|
+
@deny = directives[:deny]
|
10
|
+
@same_origin = directives[:sameorigin]
|
11
|
+
@allow_from = directives[:allow_from]
|
12
|
+
@allow_all = directives[:allowall]
|
13
|
+
end
|
14
|
+
|
15
|
+
def deny?
|
16
|
+
!!@deny
|
17
|
+
end
|
18
|
+
|
19
|
+
def same_origin?
|
20
|
+
!!@same_origin
|
21
|
+
end
|
22
|
+
|
23
|
+
def allow_all?
|
24
|
+
!!@allow_all
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
if @deny then 'DENY'
|
29
|
+
elsif @same_origin then 'SAMEORIGIN'
|
30
|
+
elsif @allow_from then "ALLOW-FROM #{@allow_from}"
|
31
|
+
elsif @allow_all then 'ALLOWALL'
|
32
|
+
else ''
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
module Headers
|
4
|
+
class XPermittedCrossDomainPolicies
|
5
|
+
|
6
|
+
def initialize(directives={})
|
7
|
+
@none = directives[:none]
|
8
|
+
@master_only = directives[:master_only]
|
9
|
+
@by_content_type = directives[:by_content_type]
|
10
|
+
@by_ftp_filename = directives[:by_ftp_filename]
|
11
|
+
@all = directives[:all]
|
12
|
+
end
|
13
|
+
|
14
|
+
def none?
|
15
|
+
!!@none
|
16
|
+
end
|
17
|
+
|
18
|
+
def master_only?
|
19
|
+
!!@master_only
|
20
|
+
end
|
21
|
+
|
22
|
+
def by_content_type?
|
23
|
+
!!@by_content_type
|
24
|
+
end
|
25
|
+
|
26
|
+
def by_ftp_filename?
|
27
|
+
!!@by_ftp_filename
|
28
|
+
end
|
29
|
+
|
30
|
+
def all?
|
31
|
+
!!@all
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
if @none then 'none'
|
36
|
+
elsif @master_only then 'master-only'
|
37
|
+
elsif @by_content_type then 'by-content-type'
|
38
|
+
elsif @by_ftp_filename then 'by-ftp-filename'
|
39
|
+
elsif @all then 'all'
|
40
|
+
else ''
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
module Headers
|
4
|
+
class XXSSProtection
|
5
|
+
|
6
|
+
attr_reader :mode
|
7
|
+
|
8
|
+
attr_reader :report
|
9
|
+
|
10
|
+
def initialize(directives={})
|
11
|
+
@enabled = directives[:enabled]
|
12
|
+
@mode = directives[:mode]
|
13
|
+
@report = directives[:report]
|
14
|
+
end
|
15
|
+
|
16
|
+
def enabled?
|
17
|
+
!!@enabled
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
str = if @enabled then '1'
|
22
|
+
else '0'
|
23
|
+
end
|
24
|
+
|
25
|
+
str << "; mode=#{@mode}" if @mode
|
26
|
+
str << "; report=#{@report}" if @report
|
27
|
+
|
28
|
+
return str
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module HTTP
|
2
|
+
module Security
|
3
|
+
class MalformedHeader
|
4
|
+
|
5
|
+
# Raw value of the header.
|
6
|
+
#
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
# Cause of the parser failure.
|
11
|
+
#
|
12
|
+
# @return [Parslet::Cause]
|
13
|
+
attr_reader :cause
|
14
|
+
|
15
|
+
#
|
16
|
+
# Initializes the malformed header.
|
17
|
+
#
|
18
|
+
# @param [String] value
|
19
|
+
# The raw header value.
|
20
|
+
#
|
21
|
+
# @param [Parslet::Cause] cause
|
22
|
+
# The cause of the parser failure.
|
23
|
+
#
|
24
|
+
def initialize(value,cause)
|
25
|
+
@value = value
|
26
|
+
@cause = cause
|
27
|
+
end
|
28
|
+
|
29
|
+
alias value to_s
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'http/security/parsers/cache_control'
|
2
|
+
require 'http/security/parsers/content_security_policy'
|
3
|
+
require 'http/security/parsers/content_security_policy_report_only'
|
4
|
+
require 'http/security/parsers/expires'
|
5
|
+
require 'http/security/parsers/pragma'
|
6
|
+
require 'http/security/parsers/public_key_pins'
|
7
|
+
require 'http/security/parsers/public_key_pins_report_only'
|
8
|
+
require 'http/security/parsers/strict_transport_security'
|
9
|
+
require 'http/security/parsers/set_cookie'
|
10
|
+
require 'http/security/parsers/public_key_pins'
|
11
|
+
require 'http/security/parsers/x_content_type_options'
|
12
|
+
require 'http/security/parsers/x_frame_options'
|
13
|
+
require 'http/security/parsers/x_permitted_cross_domain_policies'
|
14
|
+
require 'http/security/parsers/x_xss_protection'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'http/security/parsers/parser'
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
module Security
|
5
|
+
module Parsers
|
6
|
+
class CacheControl < Parser
|
7
|
+
# Cache-Control
|
8
|
+
# Syntax:
|
9
|
+
# Cache-Control = "Cache-Control" ":" 1#cache-directive
|
10
|
+
# cache-directive = cache-response-directive
|
11
|
+
# cache-response-directive =
|
12
|
+
# "public" ; Section 14.9.1
|
13
|
+
# | "private" [ "=" <"> 1#field-name <"> ] ; Section 14.9.1
|
14
|
+
# | "no-cache" [ "=" <"> 1#field-name <"> ]; Section 14.9.1
|
15
|
+
# | "no-store" ; Section 14.9.2
|
16
|
+
# | "no-transform" ; Section 14.9.5
|
17
|
+
# | "must-revalidate" ; Section 14.9.4
|
18
|
+
# | "proxy-revalidate" ; Section 14.9.4
|
19
|
+
# | "max-age" "=" delta-seconds ; Section 14.9.3
|
20
|
+
# | "s-maxage" "=" delta-seconds ; Section 14.9.3
|
21
|
+
# | cache-extension ; Section 14.9.6
|
22
|
+
# cache-extension = token [ "=" ( token | quoted-string ) ]
|
23
|
+
rule(:cache_control) do
|
24
|
+
(
|
25
|
+
cache_control_values >> (comma >> cache_control_values).repeat
|
26
|
+
).as(:directives)
|
27
|
+
end
|
28
|
+
root :cache_control
|
29
|
+
|
30
|
+
rule(:cache_control_values) do
|
31
|
+
cc_public |
|
32
|
+
cc_private |
|
33
|
+
no_cache |
|
34
|
+
no_store |
|
35
|
+
no_transform |
|
36
|
+
must_revalidate |
|
37
|
+
max_age |
|
38
|
+
s_maxage |
|
39
|
+
only_if_cached |
|
40
|
+
header_extension
|
41
|
+
end
|
42
|
+
|
43
|
+
#"private" [ "=" <"> 1#field-name <"> ];
|
44
|
+
rule(:cc_public) do
|
45
|
+
stri("public").as(:key) >> (equals >> field_name.as(:value)).maybe
|
46
|
+
end
|
47
|
+
|
48
|
+
#"private" [ "=" <"> 1#field-name <"> ];
|
49
|
+
rule(:cc_private) do
|
50
|
+
stri("private").as(:key) >> (equals >> field_name.as(:value)).maybe
|
51
|
+
end
|
52
|
+
|
53
|
+
field_directive_rule :no_cache, 'no-cache'
|
54
|
+
directive_rule :no_store, 'no-store'
|
55
|
+
directive_rule :no_transform, 'no-transform'
|
56
|
+
directive_rule :must_revalidate, 'must-revalidate'
|
57
|
+
directive_rule :only_if_cached, 'only-if-cached'
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'http/security/parsers/parser'
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
module Security
|
5
|
+
module Parsers
|
6
|
+
class ContentSecurityPolicy < Parser
|
7
|
+
# Content-Security-Policy
|
8
|
+
# Syntax:
|
9
|
+
# Content-Security-Policy =
|
10
|
+
# policy-token = [ directive-token *( ";" [ directive-token ] ) ]
|
11
|
+
# directive-token = *WSP [ directive-name [ WSP directive-value ] ]
|
12
|
+
# directive-name = 1*( ALPHA / DIGIT / "-" )
|
13
|
+
# directive-value = *( WSP / <VCHAR except ";" and ","> )
|
14
|
+
#
|
15
|
+
# Parsing Policies:
|
16
|
+
# To parse the policy policy, the user agent MUST use an algorithm equivalent to the following:
|
17
|
+
# 1. Let the set of directives be the empty set.
|
18
|
+
# 2. For each non-empty token returned by strictly splitting the string policy on the character U+003B SEMICOLON (;):
|
19
|
+
# 1. Skip whitespace.
|
20
|
+
# 2. Collect a sequence of characters that are not space characters. The collected characters are the directive name.
|
21
|
+
# 3. If there are characters remaining in token, skip ahead exactly one character (which must be a space character).
|
22
|
+
# 4. The remaining characters in token (if any) are the directive value.
|
23
|
+
# 5. If the set of directives already contains a directive whose name is a case insensitive match for directive name,
|
24
|
+
# ignore this instance of the directive and continue to the next token.
|
25
|
+
# 6. Add a directive to the set of directives with name directive name and value directive value.
|
26
|
+
# 3. Return the set of directives.
|
27
|
+
rule(:csp_pattern) do
|
28
|
+
(
|
29
|
+
csp_entry >> ( str(";") >> wsp >> csp_entry ).repeat(0) >> semicolon.maybe
|
30
|
+
).as(:directives)
|
31
|
+
end
|
32
|
+
root :csp_pattern
|
33
|
+
|
34
|
+
rule(:csp_entry) do
|
35
|
+
(csp_directive.as(:key) >> wsp >> source_list.as(:value)) |
|
36
|
+
report_uri |
|
37
|
+
sandbox
|
38
|
+
end
|
39
|
+
|
40
|
+
rule(:csp_directive) do
|
41
|
+
stri("default-src") |
|
42
|
+
stri("script-src") |
|
43
|
+
stri("object-src") |
|
44
|
+
stri("style-src") |
|
45
|
+
stri("img-src") |
|
46
|
+
stri("media-src") |
|
47
|
+
stri("frame-src") |
|
48
|
+
stri("font-src") |
|
49
|
+
stri("connect-src")
|
50
|
+
end
|
51
|
+
|
52
|
+
# Source list
|
53
|
+
# Syntax:
|
54
|
+
# source-list = *WSP [ source-expression *( 1*WSP source-expression ) *WSP ]
|
55
|
+
# / *WSP "'none'" *WSP
|
56
|
+
# source-expression = scheme-source / host-source / keyword-source
|
57
|
+
# scheme-source = scheme ":"
|
58
|
+
# host-source = [ scheme "://" ] host [ port ]
|
59
|
+
# ext-host-source = host-source "/" *( <VCHAR except ";" and ","> )
|
60
|
+
# ; ext-host-source is reserved for future use.
|
61
|
+
# keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'"
|
62
|
+
# scheme = <scheme production from RFC 3986>
|
63
|
+
# host = "*" / [ "*." ] 1*host-char *( "." 1*host-char )
|
64
|
+
# host-char = ALPHA / DIGIT / "-"
|
65
|
+
# port = ":" ( 1*DIGIT / "*" )
|
66
|
+
rule(:source_list) do
|
67
|
+
(wsp? >> stri("'none'") >> wsp?) |
|
68
|
+
(wsp? >> source_expression >> (wsp >> source_expression).repeat(0))
|
69
|
+
end
|
70
|
+
|
71
|
+
rule(:source_expression) do
|
72
|
+
scheme_source | host_source | keyword_source
|
73
|
+
end
|
74
|
+
|
75
|
+
rule(:csp_vchar) do
|
76
|
+
match["\x20-\x2b"] |
|
77
|
+
match["#{Regexp.escape("\x2d")}-\x3a"] |
|
78
|
+
match["\x3c-\x7f"]
|
79
|
+
end
|
80
|
+
|
81
|
+
rule(:scheme_source) do
|
82
|
+
(scheme >> str("://")).absent? >> scheme >> str(":")
|
83
|
+
end
|
84
|
+
|
85
|
+
rule(:host_source) do
|
86
|
+
(scheme >> str("://")).maybe >> csp_host >> port.maybe
|
87
|
+
end
|
88
|
+
|
89
|
+
rule(:csp_host) do
|
90
|
+
(str("*.").maybe >> host_char.repeat(1) >> ( str(".") >> host_char.repeat(1) ).repeat(0)) |
|
91
|
+
str("*")
|
92
|
+
end
|
93
|
+
|
94
|
+
rule(:host_char) do
|
95
|
+
alnum | str("-")
|
96
|
+
end
|
97
|
+
|
98
|
+
rule(:keyword_source) do
|
99
|
+
stri("'self'") | stri("'unsafe-inline'") | stri("'unsafe-eval'")
|
100
|
+
end
|
101
|
+
|
102
|
+
rule(:port) do
|
103
|
+
str(":") >> digits.as(:port)
|
104
|
+
end
|
105
|
+
|
106
|
+
rule(:ext_host_source) do
|
107
|
+
(scheme >> str("://")).maybe >> csp_host >> ext_host_source.maybe >> port.maybe
|
108
|
+
end
|
109
|
+
|
110
|
+
# report-uri
|
111
|
+
# directive-name = "report-uri"
|
112
|
+
# directive-value = uri-reference *( 1*WSP uri-reference )
|
113
|
+
# uri-reference = <URI-reference from RFC 3986>
|
114
|
+
rule(:report_uri) do
|
115
|
+
stri("report-uri").as(:key) >> wsp.repeat(1) >> (uri >> (wsp.repeat(1) >> uri).repeat(0)).as(:values)
|
116
|
+
end
|
117
|
+
|
118
|
+
# sandbox (Optional)
|
119
|
+
# directive-name = "sandbox"
|
120
|
+
# directive-value = token *( 1*WSP token )
|
121
|
+
# token = <token from RFC 2616>
|
122
|
+
rule(:sandbox) do
|
123
|
+
stri("sandbox").as(:key) >> wsp.repeat(1) >> (token >> (wsp.repeat(1) >> token).repeat(0)).as(:value)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|