secure_headers 3.4.0 → 4.0.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +8 -4
  6. data/CHANGELOG.md +113 -1
  7. data/CODE_OF_CONDUCT.md +46 -0
  8. data/CONTRIBUTING.md +41 -0
  9. data/Gemfile +8 -4
  10. data/Guardfile +1 -0
  11. data/LICENSE +4 -199
  12. data/README.md +63 -333
  13. data/Rakefile +22 -18
  14. data/docs/HPKP.md +17 -0
  15. data/docs/cookies.md +64 -0
  16. data/docs/hashes.md +64 -0
  17. data/docs/named_overrides_and_appends.md +107 -0
  18. data/docs/per_action_configuration.md +133 -0
  19. data/docs/sinatra.md +25 -0
  20. data/lib/secure_headers/configuration.rb +91 -44
  21. data/lib/secure_headers/hash_helper.rb +2 -1
  22. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  23. data/lib/secure_headers/headers/content_security_policy.rb +110 -51
  24. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  25. data/lib/secure_headers/headers/cookie.rb +21 -3
  26. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  27. data/lib/secure_headers/headers/policy_management.rb +143 -69
  28. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  29. data/lib/secure_headers/headers/referrer_policy.rb +5 -1
  30. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  31. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  32. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  33. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  35. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  36. data/lib/secure_headers/middleware.rb +10 -9
  37. data/lib/secure_headers/railtie.rb +7 -6
  38. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  39. data/lib/secure_headers/view_helper.rb +12 -3
  40. data/lib/secure_headers.rb +137 -55
  41. data/lib/tasks/tasks.rake +2 -1
  42. data/secure_headers.gemspec +13 -3
  43. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  44. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  45. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +59 -26
  46. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  47. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  48. data/spec/lib/secure_headers/headers/policy_management_spec.rb +78 -40
  49. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  50. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +22 -3
  51. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  52. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  53. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  54. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  56. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  57. data/spec/lib/secure_headers/middleware_spec.rb +39 -19
  58. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  59. data/spec/lib/secure_headers_spec.rb +304 -56
  60. data/spec/spec_helper.rb +13 -24
  61. data/upgrading-to-4-0.md +55 -0
  62. metadata +29 -7
@@ -1,37 +1,46 @@
1
- require_relative 'policy_management'
2
- require 'useragent'
1
+ # frozen_string_literal: true
2
+ require_relative "policy_management"
3
+ require_relative "content_security_policy_config"
4
+ require "useragent"
3
5
 
4
6
  module SecureHeaders
5
- class ContentSecurityPolicyConfigError < StandardError; end
6
7
  class ContentSecurityPolicy
7
8
  include PolicyManagement
8
9
 
9
10
  # constants to be used for version-specific UA sniffing
10
11
  VERSION_46 = ::UserAgent::Version.new("46")
12
+ VERSION_10 = ::UserAgent::Version.new("10")
13
+ FALLBACK_VERSION = ::UserAgent::Version.new("0")
11
14
 
12
15
  def initialize(config = nil, user_agent = OTHER)
13
- @config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG)
16
+ @config = if config.is_a?(Hash)
17
+ if config[:report_only]
18
+ ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
19
+ else
20
+ ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
21
+ end
22
+ elsif config.nil?
23
+ ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
24
+ else
25
+ config
26
+ end
27
+
14
28
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
15
29
  user_agent
16
30
  else
17
31
  UserAgent.parse(user_agent)
18
32
  end
19
- normalize_child_frame_src
20
- @report_only = @config[:report_only]
21
- @preserve_schemes = @config[:preserve_schemes]
22
- @script_nonce = @config[:script_nonce]
23
- @style_nonce = @config[:style_nonce]
33
+ @frame_src = normalize_child_frame_src
34
+ @preserve_schemes = @config.preserve_schemes
35
+ @script_nonce = @config.script_nonce
36
+ @style_nonce = @config.style_nonce
24
37
  end
25
38
 
26
39
  ##
27
40
  # Returns the name to use for the header. Either "Content-Security-Policy" or
28
41
  # "Content-Security-Policy-Report-Only"
29
42
  def name
30
- if @report_only
31
- REPORT_ONLY
32
- else
33
- HEADER_NAME
34
- end
43
+ @config.class.const_get(:HEADER_NAME)
35
44
  end
36
45
 
37
46
  ##
@@ -46,20 +55,12 @@ module SecureHeaders
46
55
 
47
56
  private
48
57
 
49
- # frame-src is deprecated, child-src is being implemented. They are
50
- # very similar and in most cases, the same value can be used for both.
51
58
  def normalize_child_frame_src
52
- if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src]
53
- Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
54
- elsif @config[:frame_src]
55
- Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.")
59
+ if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
60
+ raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
56
61
  end
57
62
 
58
- if supported_directives.include?(:child_src)
59
- @config[:child_src] = @config[:child_src] || @config[:frame_src]
60
- else
61
- @config[:frame_src] = @config[:frame_src] || @config[:child_src]
62
- end
63
+ @config.frame_src || @config.child_src
63
64
  end
64
65
 
65
66
  # Private: converts the config object into a string representing a policy.
@@ -73,44 +74,90 @@ module SecureHeaders
73
74
  directives.map do |directive_name|
74
75
  case DIRECTIVE_VALUE_TYPES[directive_name]
75
76
  when :boolean
76
- symbol_to_hyphen_case(directive_name) if @config[directive_name]
77
- when :string
78
- [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
79
- else
80
- build_directive(directive_name)
77
+ symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
78
+ when :sandbox_list
79
+ build_sandbox_list_directive(directive_name)
80
+ when :media_type_list
81
+ build_media_type_list_directive(directive_name)
82
+ when :source_list
83
+ build_source_list_directive(directive_name)
81
84
  end
82
85
  end.compact.join("; ")
83
86
  end
84
87
 
88
+ def build_sandbox_list_directive(directive)
89
+ return unless sandbox_list = @config.directive_value(directive)
90
+ max_strict_policy = case sandbox_list
91
+ when Array
92
+ sandbox_list.empty?
93
+ when true
94
+ true
95
+ else
96
+ false
97
+ end
98
+
99
+ # A maximally strict sandbox policy is just the `sandbox` directive,
100
+ # whith no configuraiton values.
101
+ if max_strict_policy
102
+ symbol_to_hyphen_case(directive)
103
+ elsif sandbox_list && sandbox_list.any?
104
+ [
105
+ symbol_to_hyphen_case(directive),
106
+ sandbox_list.uniq
107
+ ].join(" ")
108
+ end
109
+ end
110
+
111
+ def build_media_type_list_directive(directive)
112
+ return unless media_type_list = @config.directive_value(directive)
113
+ if media_type_list && media_type_list.any?
114
+ [
115
+ symbol_to_hyphen_case(directive),
116
+ media_type_list.uniq
117
+ ].join(" ")
118
+ end
119
+ end
120
+
85
121
  # Private: builds a string that represents one directive in a minified form.
86
122
  #
87
123
  # directive_name - a symbol representing the various ALL_DIRECTIVES
88
124
  #
89
125
  # Returns a string representing a directive.
90
- def build_directive(directive)
91
- return if @config[directive].nil?
92
-
93
- source_list = @config[directive].compact
94
- return if source_list.empty?
126
+ def build_source_list_directive(directive)
127
+ source_list = case directive
128
+ when :child_src
129
+ if supported_directives.include?(:child_src)
130
+ @frame_src
131
+ end
132
+ when :frame_src
133
+ unless supported_directives.include?(:child_src)
134
+ @frame_src
135
+ end
136
+ else
137
+ @config.directive_value(directive)
138
+ end
95
139
 
96
- normalized_source_list = minify_source_list(directive, source_list)
97
- [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
140
+ if source_list != OPT_OUT && source_list && source_list.any?
141
+ normalized_source_list = minify_source_list(directive, source_list)
142
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
143
+ end
98
144
  end
99
145
 
100
146
  # If a directive contains *, all other values are omitted.
101
147
  # If a directive contains 'none' but has other values, 'none' is ommitted.
102
148
  # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
103
149
  def minify_source_list(directive, source_list)
150
+ source_list = source_list.compact
104
151
  if source_list.include?(STAR)
105
152
  keep_wildcard_sources(source_list)
106
153
  else
107
- populate_nonces!(directive, source_list)
108
- reject_all_values_if_none!(source_list)
154
+ source_list = populate_nonces(directive, source_list)
155
+ source_list = reject_all_values_if_none(source_list)
109
156
 
110
157
  unless directive == REPORT_URI || @preserve_schemes
111
- strip_source_schemes!(source_list)
158
+ source_list = strip_source_schemes(source_list)
112
159
  end
113
- dedup_source_list(source_list).join(" ")
160
+ dedup_source_list(source_list)
114
161
  end
115
162
  end
116
163
 
@@ -120,8 +167,12 @@ module SecureHeaders
120
167
  end
121
168
 
122
169
  # Discard any 'none' values if more directives are supplied since none may override values.
123
- def reject_all_values_if_none!(source_list)
124
- source_list.reject! { |value| value == NONE } if source_list.length > 1
170
+ def reject_all_values_if_none(source_list)
171
+ if source_list.length > 1
172
+ source_list.reject { |value| value == NONE }
173
+ else
174
+ source_list
175
+ end
125
176
  end
126
177
 
127
178
  # Removes duplicates and sources that already match an existing wild card.
@@ -143,12 +194,14 @@ module SecureHeaders
143
194
 
144
195
  # Private: append a nonce to the script/style directories if script_nonce
145
196
  # or style_nonce are provided.
146
- def populate_nonces!(directive, source_list)
197
+ def populate_nonces(directive, source_list)
147
198
  case directive
148
199
  when SCRIPT_SRC
149
200
  append_nonce(source_list, @script_nonce)
150
201
  when STYLE_SRC
151
202
  append_nonce(source_list, @style_nonce)
203
+ else
204
+ source_list
152
205
  end
153
206
  end
154
207
 
@@ -165,19 +218,23 @@ module SecureHeaders
165
218
  source_list << UNSAFE_INLINE
166
219
  end
167
220
  end
221
+
222
+ source_list
168
223
  end
169
224
 
170
225
  # Private: return the list of directives that are supported by the user agent,
171
226
  # starting with default-src and ending with report-uri.
172
227
  def directives
173
- [DEFAULT_SRC,
228
+ [
229
+ DEFAULT_SRC,
174
230
  BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
175
- REPORT_URI].flatten.select { |directive| @config.key?(directive) }
231
+ REPORT_URI
232
+ ].flatten
176
233
  end
177
234
 
178
235
  # Private: Remove scheme from source expressions.
179
- def strip_source_schemes!(source_list)
180
- source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
236
+ def strip_source_schemes(source_list)
237
+ source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
181
238
  end
182
239
 
183
240
  # Private: determine which directives are supported for the given user agent.
@@ -187,8 +244,10 @@ module SecureHeaders
187
244
  # Returns an array of symbols representing the directives.
188
245
  def supported_directives
189
246
  @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
190
- if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
247
+ if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
191
248
  VARIATIONS["FirefoxTransitional"]
249
+ elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
250
+ VARIATIONS["SafariTransitional"]
192
251
  else
193
252
  VARIATIONS[@parsed_ua.browser]
194
253
  end
@@ -198,11 +257,11 @@ module SecureHeaders
198
257
  end
199
258
 
200
259
  def nonces_supported?
201
- @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
260
+ @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
202
261
  end
203
262
 
204
263
  def symbol_to_hyphen_case(sym)
205
- sym.to_s.tr('_', '-')
264
+ sym.to_s.tr("_", "-")
206
265
  end
207
266
  end
208
267
  end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ module DynamicConfig
4
+ def self.included(base)
5
+ base.send(:attr_writer, :modified)
6
+ base.send(:attr_reader, *base.attrs)
7
+ base.attrs.each do |attr|
8
+ base.send(:define_method, "#{attr}=") do |value|
9
+ if self.class.attrs.include?(attr)
10
+ write_attribute(attr, value)
11
+ else
12
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(hash)
19
+ @base_uri = nil
20
+ @block_all_mixed_content = nil
21
+ @child_src = nil
22
+ @connect_src = nil
23
+ @default_src = nil
24
+ @font_src = nil
25
+ @form_action = nil
26
+ @frame_ancestors = nil
27
+ @frame_src = nil
28
+ @img_src = nil
29
+ @manifest_src = nil
30
+ @media_src = nil
31
+ @object_src = nil
32
+ @plugin_types = nil
33
+ @preserve_schemes = nil
34
+ @report_only = nil
35
+ @report_uri = nil
36
+ @sandbox = nil
37
+ @script_nonce = nil
38
+ @script_src = nil
39
+ @style_nonce = nil
40
+ @style_src = nil
41
+ @upgrade_insecure_requests = nil
42
+
43
+ from_hash(hash)
44
+ @modified = false
45
+ end
46
+
47
+ def update_directive(directive, value)
48
+ self.send("#{directive}=", value)
49
+ end
50
+
51
+ def directive_value(directive)
52
+ if self.class.attrs.include?(directive)
53
+ self.send(directive)
54
+ end
55
+ end
56
+
57
+ def modified?
58
+ @modified
59
+ end
60
+
61
+ def merge(new_hash)
62
+ ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
63
+ end
64
+
65
+ def merge!(new_hash)
66
+ from_hash(new_hash)
67
+ end
68
+
69
+ def append(new_hash)
70
+ from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
71
+ end
72
+
73
+ def to_h
74
+ self.class.attrs.each_with_object({}) do |key, hash|
75
+ value = self.send(key)
76
+ hash[key] = value unless value.nil?
77
+ end
78
+ end
79
+
80
+ def dup
81
+ self.class.new(self.to_h)
82
+ end
83
+
84
+ def opt_out?
85
+ false
86
+ end
87
+
88
+ def ==(o)
89
+ self.class == o.class && self.to_h == o.to_h
90
+ end
91
+
92
+ alias_method :[], :directive_value
93
+ alias_method :[]=, :update_directive
94
+
95
+ private
96
+ def from_hash(hash)
97
+ hash.each_pair do |k, v|
98
+ next if v.nil?
99
+
100
+ if self.class.attrs.include?(k)
101
+ write_attribute(k, v)
102
+ else
103
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def write_attribute(attr, value)
109
+ value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
110
+ attr_variable = "@#{attr}"
111
+ prev_value = self.instance_variable_get(attr_variable)
112
+ self.instance_variable_set(attr_variable, value)
113
+ if prev_value != value
114
+ @modified = true
115
+ end
116
+ end
117
+ end
118
+
119
+ class ContentSecurityPolicyConfigError < StandardError; end
120
+ class ContentSecurityPolicyConfig
121
+ CONFIG_KEY = :csp
122
+ HEADER_NAME = "Content-Security-Policy".freeze
123
+
124
+ ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
125
+ def self.attrs
126
+ ATTRS
127
+ end
128
+
129
+ include DynamicConfig
130
+
131
+ # based on what was suggested in https://github.com/rails/rails/pull/24961/files
132
+ DEFAULT = {
133
+ default_src: %w('self' https:),
134
+ font_src: %w('self' https: data:),
135
+ img_src: %w('self' https: data:),
136
+ object_src: %w('none'),
137
+ script_src: %w(https:),
138
+ style_src: %w('self' https: 'unsafe-inline')
139
+ }
140
+
141
+ def report_only?
142
+ false
143
+ end
144
+
145
+ def make_report_only
146
+ ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
147
+ end
148
+ end
149
+
150
+ class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
151
+ CONFIG_KEY = :csp_report_only
152
+ HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
153
+
154
+ def report_only?
155
+ true
156
+ end
157
+
158
+ def make_report_only
159
+ self
160
+ end
161
+ end
162
+ end
@@ -1,5 +1,7 @@
1
- require 'cgi'
2
- require 'secure_headers/utils/cookies_config'
1
+ # frozen_string_literal: true
2
+ require "cgi"
3
+ require "secure_headers/utils/cookies_config"
4
+
3
5
 
4
6
  module SecureHeaders
5
7
  class CookiesConfigError < StandardError; end
@@ -13,8 +15,18 @@ module SecureHeaders
13
15
 
14
16
  attr_reader :raw_cookie, :config
15
17
 
18
+ COOKIE_DEFAULTS = {
19
+ httponly: true,
20
+ secure: true,
21
+ samesite: { lax: true },
22
+ }.freeze
23
+
16
24
  def initialize(cookie, config)
17
25
  @raw_cookie = cookie
26
+ unless config == OPT_OUT
27
+ config ||= {}
28
+ config = COOKIE_DEFAULTS.merge(config)
29
+ end
18
30
  @config = config
19
31
  @attributes = {
20
32
  httponly: nil,
@@ -56,6 +68,7 @@ module SecureHeaders
56
68
  end
57
69
 
58
70
  def flag_cookie?(attribute)
71
+ return false if config == OPT_OUT
59
72
  case config[attribute]
60
73
  when TrueClass
61
74
  true
@@ -85,6 +98,7 @@ module SecureHeaders
85
98
  end
86
99
 
87
100
  def flag_samesite?
101
+ return false if config == OPT_OUT || config[:samesite] == OPT_OUT
88
102
  flag_samesite_lax? || flag_samesite_strict?
89
103
  end
90
104
 
@@ -99,6 +113,10 @@ module SecureHeaders
99
113
  def flag_samesite_enforcement?(mode)
100
114
  return unless config[:samesite]
101
115
 
116
+ if config[:samesite].is_a?(TrueClass) && mode == :lax
117
+ return true
118
+ end
119
+
102
120
  case config[:samesite][mode]
103
121
  when Hash
104
122
  conditionally_flag?(config[:samesite][mode])
@@ -113,7 +131,7 @@ module SecureHeaders
113
131
  return unless cookie
114
132
 
115
133
  cookie.split(/[;,]\s?/).each do |pairs|
116
- name, values = pairs.split('=',2)
134
+ name, values = pairs.split("=", 2)
117
135
  name = CGI.unescape(name)
118
136
 
119
137
  attribute = name.downcase.to_sym
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ExpectCertificateTransparencyConfigError < StandardError; end
4
+
5
+ class ExpectCertificateTransparency
6
+ HEADER_NAME = "Expect-CT".freeze
7
+ CONFIG_KEY = :expect_certificate_transparency
8
+ INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
+ INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
+ REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
11
+ INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
12
+
13
+ class << self
14
+ # Public: Generate a Expect-CT header.
15
+ #
16
+ # Returns nil if not configured, returns header name and value if
17
+ # configured.
18
+ def make_header(config)
19
+ return if config.nil?
20
+
21
+ header = new(config)
22
+ [HEADER_NAME, header.value]
23
+ end
24
+
25
+ def validate_config!(config)
26
+ return if config.nil? || config == OPT_OUT
27
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
28
+
29
+ unless [true, false, nil].include?(config[:enforce])
30
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
31
+ end
32
+
33
+ if !config[:max_age]
34
+ raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
35
+ elsif config[:max_age].to_s !~ /\A\d+\z/
36
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(config)
42
+ @enforced = config.fetch(:enforce, nil)
43
+ @max_age = config.fetch(:max_age, nil)
44
+ @report_uri = config.fetch(:report_uri, nil)
45
+ end
46
+
47
+ def value
48
+ [
49
+ enforced_directive,
50
+ max_age_directive,
51
+ report_uri_directive
52
+ ].compact.join("; ").strip
53
+ end
54
+
55
+ def enforced_directive
56
+ # Unfortunately `if @enforced` isn't enough here in case someone
57
+ # passes in a random string so let's be specific with it to prevent
58
+ # accidental enforcement.
59
+ "enforce" if @enforced == true
60
+ end
61
+
62
+ def max_age_directive
63
+ "max-age=#{@max_age}" if @max_age
64
+ end
65
+
66
+ def report_uri_directive
67
+ "report-uri=\"#{@report_uri}\"" if @report_uri
68
+ end
69
+ end
70
+ end