flipper-cloud 0.28.3 → 1.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.
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