http-security 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|