flipper-cloud 0.28.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50ce1804252b0dc363fbc55f71df45a21189e41cc003044b6ee5e970fcdf384d
4
- data.tar.gz: 1a0e0552a09a3c3979f2e07a1fb87e0dc7ddb7fa4d20fdbcb74985323e7561cd
3
+ metadata.gz: c22781f15c68a4a2bcc6fbcde3aca4336e4ddaaaec7c56702bafc78faaeefb11
4
+ data.tar.gz: 4c60f975ce2f1922fc10f6184b4a9863d48041cc53aa2c3fd61895dd80762fe4
5
5
  SHA512:
6
- metadata.gz: 901e42974dbb76020bcb7ca6394ad5951fcfe18e00b2a4ea735a2cfabd8965953b8867a3bb5184c7161aeb9c611e7aa205a062cae5a1d99aa653f7e88d35882b
7
- data.tar.gz: 21e0c1738b9b34203388bae07240fb1ffb6ad82163b59bb4abbd9796af02c513551639d1ce3bacd7939cad6f4088b240792200baed8724c0eefe7325dcbaae8a
6
+ metadata.gz: cd91a1b89e4782041a2c23d84ede4efae64ff15decc689820bce99456a948afeb70de5e4ba3e60b308a71a4303307b8f17be31c310e527661e8dd77814d5fe7b
7
+ data.tar.gz: 2d6699d34f35a04593f5b973ac86517a8ada7ba41dc7924dcb5832796cb5687c19a2f0e84c90d67fe1c4f8c1a0dc45ad9679be202974619e15447fcb8dbaa7a4
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.28.3'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-21 00:00:00.000000000 Z
11
+ date: 2023-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: flipper
@@ -16,64 +16,31 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.28.3
19
+ version: 1.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.28.3
27
- - !ruby/object:Gem::Dependency
28
- name: brow
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 0.4.1
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 0.4.1
26
+ version: 1.0.0
41
27
  description:
42
- email:
43
- - nunemaker@gmail.com
28
+ email: support@flippercloud.io
44
29
  executables: []
45
30
  extensions: []
46
31
  extra_rdoc_files: []
47
32
  files:
48
- - docs/images/flipper_cloud.png
49
- - examples/cloud/app.ru
50
- - examples/cloud/basic.rb
51
- - examples/cloud/cloud_setup.rb
52
- - examples/cloud/forked.rb
53
- - examples/cloud/import.rb
54
- - examples/cloud/threaded.rb
55
- - flipper-cloud.gemspec
56
33
  - lib/flipper-cloud.rb
57
- - lib/flipper/cloud.rb
58
- - lib/flipper/cloud/configuration.rb
59
- - lib/flipper/cloud/dsl.rb
60
- - lib/flipper/cloud/engine.rb
61
- - lib/flipper/cloud/instrumenter.rb
62
- - lib/flipper/cloud/message_verifier.rb
63
- - lib/flipper/cloud/middleware.rb
64
- - lib/flipper/cloud/routes.rb
65
34
  - lib/flipper/version.rb
66
- - spec/flipper/cloud/configuration_spec.rb
67
- - spec/flipper/cloud/dsl_spec.rb
68
- - spec/flipper/cloud/engine_spec.rb
69
- - spec/flipper/cloud/message_verifier_spec.rb
70
- - spec/flipper/cloud/middleware_spec.rb
71
- - spec/flipper/cloud_spec.rb
72
- homepage: https://github.com/jnunemaker/flipper
35
+ homepage: https://www.flippercloud.io
73
36
  licenses:
74
37
  - MIT
75
38
  metadata:
76
- changelog_uri: https://github.com/jnunemaker/flipper/blob/main/Changelog.md
39
+ documentation_uri: https://www.flippercloud.io/docs
40
+ homepage_uri: https://www.flippercloud.io
41
+ source_code_uri: https://github.com/flippercloud/flipper
42
+ bug_tracker_uri: https://github.com/flippercloud/flipper/issues
43
+ changelog_uri: https://github.com/flippercloud/flipper/blob/main/Changelog.md
77
44
  post_install_message:
78
45
  rdoc_options: []
79
46
  require_paths:
@@ -89,14 +56,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
56
  - !ruby/object:Gem::Version
90
57
  version: '0'
91
58
  requirements: []
92
- rubygems_version: 3.3.7
59
+ rubygems_version: 3.4.10
93
60
  signing_key:
94
61
  specification_version: 4
95
- summary: FlipperCloud.io adapter for Flipper
96
- test_files:
97
- - spec/flipper/cloud/configuration_spec.rb
98
- - spec/flipper/cloud/dsl_spec.rb
99
- - spec/flipper/cloud/engine_spec.rb
100
- - spec/flipper/cloud/message_verifier_spec.rb
101
- - spec/flipper/cloud/middleware_spec.rb
102
- - spec/flipper/cloud_spec.rb
62
+ summary: "[DEPRECATED] This gem has been merged into the `flipper` gem"
63
+ test_files: []
Binary file
@@ -1,12 +0,0 @@
1
- # Usage (from the repo root):
2
- # env FLIPPER_CLOUD_TOKEN=<token> FLIPPER_CLOUD_SYNC_SECRET=<secret> bundle exec rackup examples/cloud/app.ru -p 9999
3
- # http://localhost:9999/
4
-
5
- require 'bundler/setup'
6
- require 'flipper/cloud'
7
-
8
- Flipper.configure do |config|
9
- config.default { Flipper::Cloud.new }
10
- end
11
-
12
- run Flipper::Cloud.app
@@ -1,22 +0,0 @@
1
- # Usage (from the repo root):
2
- # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/basic.rb
3
-
4
- require_relative "./cloud_setup"
5
- require 'bundler/setup'
6
- require 'flipper/cloud'
7
-
8
- Flipper[:stats].enable
9
-
10
- if Flipper[:stats].enabled?
11
- puts 'Enabled!'
12
- else
13
- puts 'Disabled!'
14
- end
15
-
16
- Flipper[:stats].disable
17
-
18
- if Flipper[:stats].enabled?
19
- puts 'Enabled!'
20
- else
21
- puts 'Disabled!'
22
- end
@@ -1,4 +0,0 @@
1
- if ENV["FLIPPER_CLOUD_TOKEN"].nil? || ENV["FLIPPER_CLOUD_TOKEN"].empty?
2
- warn "FLIPPER_CLOUD_TOKEN missing so skipping cloud example."
3
- exit
4
- end
@@ -1,31 +0,0 @@
1
- # Usage (from the repo root):
2
- # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/threaded.rb
3
-
4
- require_relative "./cloud_setup"
5
- require 'bundler/setup'
6
- require 'flipper/cloud'
7
-
8
- pids = 5.times.map do |n|
9
- fork {
10
- # Check every second to see if the feature is enabled
11
- threads = []
12
- 5.times do
13
- threads << Thread.new do
14
- loop do
15
- sleep rand
16
-
17
- if Flipper[:stats].enabled?
18
- puts "#{Process.pid} #{Time.now.to_i} Enabled!"
19
- else
20
- puts "#{Process.pid} #{Time.now.to_i} Disabled!"
21
- end
22
- end
23
- end
24
- end
25
- threads.map(&:join)
26
- }
27
- end
28
-
29
- pids.each do |pid|
30
- Process.waitpid pid, 0
31
- end
@@ -1,17 +0,0 @@
1
- # Usage (from the repo root):
2
- # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/import.rb
3
-
4
- require_relative "./cloud_setup"
5
- require 'bundler/setup'
6
- require 'flipper'
7
- require 'flipper/cloud'
8
-
9
- Flipper.enable(:test)
10
- Flipper.enable(:search)
11
- Flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
12
- Flipper.enable_percentage_of_time(:logging, 5)
13
-
14
- cloud = Flipper::Cloud.new
15
-
16
- # makes cloud identical to memory flipper
17
- cloud.import(Flipper)
@@ -1,36 +0,0 @@
1
- # Usage (from the repo root):
2
- # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/threaded.rb
3
-
4
- require_relative "./cloud_setup"
5
- require 'bundler/setup'
6
- require 'flipper/cloud'
7
- require "active_support/notifications"
8
- require "active_support/isolated_execution_state"
9
-
10
- ActiveSupport::Notifications.subscribe(/poller\.flipper/) do |*args|
11
- p args: args
12
- end
13
-
14
- Flipper.configure do |config|
15
- config.default {
16
- Flipper::Cloud.new(local_adapter: config.adapter, instrumenter: ActiveSupport::Notifications)
17
- }
18
- end
19
-
20
- # Check every second to see if the feature is enabled
21
- threads = []
22
- 10.times do
23
- threads << Thread.new do
24
- loop do
25
- sleep rand
26
-
27
- if Flipper[:stats].enabled?
28
- puts "#{Time.now.to_i} Enabled!"
29
- else
30
- puts "#{Time.now.to_i} Disabled!"
31
- end
32
- end
33
- end
34
- end
35
-
36
- threads.map(&:join)
@@ -1,28 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/flipper/version', __FILE__)
3
- require File.expand_path('../lib/flipper/metadata', __FILE__)
4
-
5
- flipper_cloud_files = lambda do |file|
6
- file =~ /cloud/
7
- end
8
-
9
- Gem::Specification.new do |gem|
10
- gem.authors = ['John Nunemaker']
11
- gem.email = ['nunemaker@gmail.com']
12
- gem.summary = 'FlipperCloud.io adapter for Flipper'
13
- gem.license = 'MIT'
14
- gem.homepage = 'https://github.com/jnunemaker/flipper'
15
-
16
- extra_files = [
17
- 'lib/flipper/version.rb',
18
- ]
19
- gem.files = `git ls-files`.split("\n").select(&flipper_cloud_files) + extra_files
20
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").select(&flipper_cloud_files)
21
- gem.name = 'flipper-cloud'
22
- gem.require_paths = ['lib']
23
- gem.version = Flipper::VERSION
24
- gem.metadata = Flipper::METADATA
25
-
26
- gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
27
- gem.add_dependency "brow", "~> 0.4.1"
28
- end
@@ -1,189 +0,0 @@
1
- require "socket"
2
- require "flipper/adapters/http"
3
- require "flipper/adapters/poll"
4
- require "flipper/poller"
5
- require "flipper/adapters/memory"
6
- require "flipper/adapters/dual_write"
7
- require "flipper/adapters/sync/synchronizer"
8
- require "flipper/cloud/instrumenter"
9
- require "brow"
10
-
11
- module Flipper
12
- module Cloud
13
- class Configuration
14
- # The set of valid ways that syncing can happpen.
15
- VALID_SYNC_METHODS = Set[
16
- :poll,
17
- :webhook,
18
- ].freeze
19
-
20
- DEFAULT_URL = "https://www.flippercloud.io/adapter".freeze
21
-
22
- # Private: Keeps track of brow instances so they can be shared across
23
- # threads.
24
- def self.brow_instances
25
- @brow_instances ||= Concurrent::Map.new
26
- end
27
-
28
- # Public: The token corresponding to an environment on flippercloud.io.
29
- attr_accessor :token
30
-
31
- # Public: The url for http adapter. Really should only be customized for
32
- # development work. Feel free to forget you ever saw this.
33
- attr_reader :url
34
-
35
- # Public: net/http read timeout for all http requests (default: 5).
36
- attr_accessor :read_timeout
37
-
38
- # Public: net/http open timeout for all http requests (default: 5).
39
- attr_accessor :open_timeout
40
-
41
- # Public: net/http write timeout for all http requests (default: 5).
42
- attr_accessor :write_timeout
43
-
44
- # Public: IO stream to send debug output too. Off by default.
45
- #
46
- # # for example, this would send all http request information to STDOUT
47
- # configuration = Flipper::Cloud::Configuration.new
48
- # configuration.debug_output = STDOUT
49
- attr_accessor :debug_output
50
-
51
- # Public: Instrumenter to use for the Flipper instance returned by
52
- # Flipper::Cloud.new (default: Flipper::Instrumenters::Noop).
53
- #
54
- # # for example, to use active support notifications you could do:
55
- # configuration = Flipper::Cloud::Configuration.new
56
- # configuration.instrumenter = ActiveSupport::Notifications
57
- attr_accessor :instrumenter
58
-
59
- # Public: Local adapter that all reads should go to in order to ensure
60
- # latency is low and resiliency is high. This adapter is automatically
61
- # kept in sync with cloud.
62
- #
63
- # # for example, to use active record you could do:
64
- # configuration = Flipper::Cloud::Configuration.new
65
- # configuration.local_adapter = Flipper::Adapters::ActiveRecord.new
66
- attr_accessor :local_adapter
67
-
68
- # Public: The Integer or Float number of seconds between attempts to bring
69
- # the local in sync with cloud (default: 10).
70
- attr_accessor :sync_interval
71
-
72
- # Public: The secret used to verify if syncs in the middleware should
73
- # occur or not.
74
- attr_accessor :sync_secret
75
-
76
- def initialize(options = {})
77
- @token = options.fetch(:token) { ENV["FLIPPER_CLOUD_TOKEN"] }
78
-
79
- if @token.nil?
80
- raise ArgumentError, "Flipper::Cloud token is missing. Please set FLIPPER_CLOUD_TOKEN or provide the token (e.g. Flipper::Cloud.new(token: 'token'))."
81
- end
82
-
83
- @read_timeout = options.fetch(:read_timeout) { ENV.fetch("FLIPPER_CLOUD_READ_TIMEOUT", 5).to_f }
84
- @open_timeout = options.fetch(:open_timeout) { ENV.fetch("FLIPPER_CLOUD_OPEN_TIMEOUT", 5).to_f }
85
- @write_timeout = options.fetch(:write_timeout) { ENV.fetch("FLIPPER_CLOUD_WRITE_TIMEOUT", 5).to_f }
86
- @sync_interval = options.fetch(:sync_interval) { ENV.fetch("FLIPPER_CLOUD_SYNC_INTERVAL", 10).to_f }
87
- @sync_secret = options.fetch(:sync_secret) { ENV["FLIPPER_CLOUD_SYNC_SECRET"] }
88
- @local_adapter = options.fetch(:local_adapter) { Adapters::Memory.new }
89
- @debug_output = options[:debug_output]
90
- @adapter_block = ->(adapter) { adapter }
91
- self.url = options.fetch(:url) { ENV.fetch("FLIPPER_CLOUD_URL", DEFAULT_URL) }
92
-
93
- instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
94
-
95
- # This is alpha. Don't use this unless you are me. And you are not me.
96
- cloud_instrument = options.fetch(:cloud_instrument) { ENV["FLIPPER_CLOUD_INSTRUMENT"] == "1" }
97
- @instrumenter = if cloud_instrument
98
- Instrumenter.new(brow: brow, instrumenter: instrumenter)
99
- else
100
- instrumenter
101
- end
102
- end
103
-
104
- # Public: Read or customize the http adapter. Calling without a block will
105
- # perform a read. Calling with a block yields the cloud adapter
106
- # for customization.
107
- #
108
- # # for example, to instrument the http calls, you can wrap the http
109
- # # adapter with the intsrumented adapter
110
- # configuration = Flipper::Cloud::Configuration.new
111
- # configuration.adapter do |adapter|
112
- # Flipper::Adapters::Instrumented.new(adapter)
113
- # end
114
- #
115
- def adapter(&block)
116
- if block_given?
117
- @adapter_block = block
118
- else
119
- @adapter_block.call app_adapter
120
- end
121
- end
122
-
123
- # Public: Set url for the http adapter.
124
- attr_writer :url
125
-
126
- def sync
127
- Flipper::Adapters::Sync::Synchronizer.new(local_adapter, http_adapter, {
128
- instrumenter: instrumenter,
129
- }).call
130
- end
131
-
132
- def brow
133
- self.class.brow_instances.compute_if_absent(url + token) do
134
- uri = URI.parse(url)
135
- uri.path = "#{uri.path}/events".squeeze("/")
136
-
137
- Brow::Client.new({
138
- url: uri.to_s,
139
- headers: {
140
- "Accept" => "application/json",
141
- "Content-Type" => "application/json",
142
- "User-Agent" => "Flipper v#{VERSION} via Brow v#{Brow::VERSION}",
143
- "Flipper-Cloud-Token" => @token,
144
- }
145
- })
146
- end
147
- end
148
-
149
- # Public: The method that will be used to synchronize local adapter with
150
- # cloud. (default: :poll, will be :webhook if sync_secret is set).
151
- def sync_method
152
- sync_secret ? :webhook : :poll
153
- end
154
-
155
- private
156
-
157
- def app_adapter
158
- read_adapter = sync_method == :webhook ? local_adapter : poll_adapter
159
- Flipper::Adapters::DualWrite.new(read_adapter, http_adapter)
160
- end
161
-
162
- def poller
163
- Flipper::Poller.get(@url + @token, {
164
- interval: sync_interval,
165
- remote_adapter: http_adapter,
166
- instrumenter: instrumenter,
167
- }).tap(&:start)
168
- end
169
-
170
- def poll_adapter
171
- Flipper::Adapters::Poll.new(poller, local_adapter)
172
- end
173
-
174
- def http_adapter
175
- Flipper::Adapters::Http.new({
176
- url: @url,
177
- read_timeout: @read_timeout,
178
- open_timeout: @open_timeout,
179
- write_timeout: @write_timeout,
180
- max_retries: 0, # we'll handle retries ourselves
181
- debug_output: @debug_output,
182
- headers: {
183
- "Flipper-Cloud-Token" => @token,
184
- },
185
- })
186
- end
187
- end
188
- end
189
- end
@@ -1,27 +0,0 @@
1
- require 'forwardable'
2
-
3
- module Flipper
4
- module Cloud
5
- class DSL < SimpleDelegator
6
- attr_reader :cloud_configuration
7
-
8
- def initialize(cloud_configuration)
9
- @cloud_configuration = cloud_configuration
10
- super Flipper.new(@cloud_configuration.adapter, instrumenter: @cloud_configuration.instrumenter)
11
- end
12
-
13
- def sync
14
- @cloud_configuration.sync
15
- end
16
-
17
- def sync_secret
18
- @cloud_configuration.sync_secret
19
- end
20
-
21
- def inspect
22
- inspect_id = ::Kernel::format "%x", (object_id * 2)
23
- %(#<#{self.class}:0x#{inspect_id} @cloud_configuration=#{cloud_configuration.inspect}, flipper=#{__getobj__.inspect}>)
24
- end
25
- end
26
- end
27
- end
@@ -1,29 +0,0 @@
1
- require "flipper/railtie"
2
-
3
- module Flipper
4
- module Cloud
5
- class Engine < Rails::Engine
6
- paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
7
-
8
- config.before_configuration do
9
- config.flipper.cloud_path = "_flipper"
10
- end
11
-
12
- initializer "flipper.cloud.default", before: :load_config_initializers do |app|
13
- Flipper.configure do |config|
14
- config.default do
15
- if ENV["FLIPPER_CLOUD_TOKEN"]
16
- Flipper::Cloud.new(
17
- local_adapter: config.adapter,
18
- instrumenter: app.config.flipper.instrumenter
19
- )
20
- else
21
- warn "Missing FLIPPER_CLOUD_TOKEN environment variable. Disabling Flipper::Cloud."
22
- Flipper.new(config.adapter)
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,48 +0,0 @@
1
- require "delegate"
2
- require "flipper/instrumenters/noop"
3
-
4
- module Flipper
5
- module Cloud
6
- class Instrumenter < SimpleDelegator
7
- def initialize(options = {})
8
- @brow = options.fetch(:brow)
9
- @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
10
- super @instrumenter
11
- end
12
-
13
- def instrument(name, payload = {}, &block)
14
- result = @instrumenter.instrument(name, payload, &block)
15
- push name, payload
16
- result
17
- end
18
-
19
- private
20
-
21
- def push(name, payload)
22
- return unless name == Flipper::Feature::InstrumentationName
23
- return unless :enabled? == payload[:operation]
24
-
25
- dimensions = {
26
- "feature" => payload[:feature_name].to_s,
27
- "result" => payload[:result].to_s,
28
- }
29
-
30
- if (thing = payload[:thing])
31
- dimensions["flipper_id"] = thing.value.to_s
32
- end
33
-
34
- if (actors = payload[:actors])
35
- dimensions["flipper_ids"] = actors.map { |actor| actor.value.to_s }
36
- end
37
-
38
- event = {
39
- type: "enabled",
40
- dimensions: dimensions,
41
- measures: {},
42
- ts: Time.now.utc,
43
- }
44
- @brow.push event
45
- end
46
- end
47
- end
48
- end
@@ -1,95 +0,0 @@
1
- require "openssl"
2
- require "digest/sha2"
3
-
4
- module Flipper
5
- module Cloud
6
- class MessageVerifier
7
- class InvalidSignature < StandardError; end
8
-
9
- DEFAULT_VERSION = "v1"
10
-
11
- def self.header(signature, timestamp, version = DEFAULT_VERSION)
12
- raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
13
- raise ArgumentError, "signature should be a string" unless signature.is_a?(String)
14
- "t=#{timestamp.to_i},#{version}=#{signature}"
15
- end
16
-
17
- def initialize(secret:, version: DEFAULT_VERSION)
18
- @secret = secret
19
- @version = version || DEFAULT_VERSION
20
-
21
- raise ArgumentError, "secret should be a string" unless @secret.is_a?(String)
22
- raise ArgumentError, "version should be a string" unless @version.is_a?(String)
23
- end
24
-
25
- def generate(payload, timestamp)
26
- raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
27
- raise ArgumentError, "payload should be a string" unless payload.is_a?(String)
28
-
29
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @secret, "#{timestamp.to_i}.#{payload}")
30
- end
31
-
32
- def header(signature, timestamp)
33
- self.class.header(signature, timestamp, @version)
34
- end
35
-
36
- # Public: Verifies the signature header for a given payload.
37
- #
38
- # Raises a InvalidSignature in the following cases:
39
- # - the header does not match the expected format
40
- # - no signatures found with the expected scheme
41
- # - no signatures matching the expected signature
42
- # - a tolerance is provided and the timestamp is not within the
43
- # tolerance
44
- #
45
- # Returns true otherwise.
46
- def verify(payload, header, tolerance: nil)
47
- begin
48
- timestamp, signatures = get_timestamp_and_signatures(header)
49
- rescue StandardError
50
- raise InvalidSignature, "Unable to extract timestamp and signatures from header"
51
- end
52
-
53
- if signatures.empty?
54
- raise InvalidSignature, "No signatures found with expected version #{@version}"
55
- end
56
-
57
- expected_sig = generate(payload, timestamp)
58
- unless signatures.any? { |s| secure_compare(expected_sig, s) }
59
- raise InvalidSignature, "No signatures found matching the expected signature for payload"
60
- end
61
-
62
- if tolerance && timestamp < Time.now - tolerance
63
- raise InvalidSignature, "Timestamp outside the tolerance zone (#{Time.at(timestamp)})"
64
- end
65
-
66
- true
67
- end
68
-
69
- private
70
-
71
- # Extracts the timestamp and the signature(s) with the desired version
72
- # from the header
73
- def get_timestamp_and_signatures(header)
74
- list_items = header.split(/,\s*/).map { |i| i.split("=", 2) }
75
- timestamp = Integer(list_items.select { |i| i[0] == "t" }[0][1])
76
- signatures = list_items.select { |i| i[0] == @version }.map { |i| i[1] }
77
- [Time.at(timestamp), signatures]
78
- end
79
-
80
- # Private
81
- def fixed_length_secure_compare(a, b)
82
- raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
83
- l = a.unpack "C#{a.bytesize}"
84
- res = 0
85
- b.each_byte { |byte| res |= byte ^ l.shift }
86
- res == 0
87
- end
88
-
89
- # Private
90
- def secure_compare(a, b)
91
- fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
92
- end
93
- end
94
- end
95
- end