gsk_cache 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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/gsk_cache.rb +175 -0
  3. metadata +72 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4e4eaba75a4c51cec1231c1e554101679c450c1736e9fd1d6849ff3d3d92b0ec
4
+ data.tar.gz: 48019eda48fb59c0efbef7f71b6e06e74c1ee7312e9e129386249222ba5d8ac9
5
+ SHA512:
6
+ metadata.gz: 9d15feb37ef8bdfc36f4f4bd2c157af70214bfdc33c195726c7804f00c935a7bfc2b1354d973ec4ed745ee02611d486107f9999e5116f34363d51162647a23ef
7
+ data.tar.gz: 117c8510dd5978cce0ba8c00559a5d8b97348d761e6736875a0baf2c9e0f7bdf36649bc21edac574128ee4ce8941ac285ff959a619988a5eb3c4b4a4d959383e
data/lib/gsk_cache.rb ADDED
@@ -0,0 +1,175 @@
1
+ require 'excon'
2
+ require 'json'
3
+
4
+ module GSKCache
5
+ @@key_updater_semaphore = Mutex.new
6
+ @@key_updater_thread = nil
7
+
8
+ ##
9
+ # Keys used for signing in production
10
+ SIGNING_KEY_URL = 'https://payments.developers.google.com/paymentmethodtoken/keys.json'.freeze
11
+
12
+ ##
13
+ # Keys used for signing in a testing environment
14
+ TEST_SIGNING_KEY_URL = 'https://payments.developers.google.com/paymentmethodtoken/test/keys.json'.freeze
15
+
16
+ MAX_CACHE_TIMEOUT = 31 * 24 * 3600 # One month
17
+ MIN_CACHE_TIMEOUT = 600 # 10 minutes
18
+
19
+ BOOT_WAIT_TIME_MAX = 8
20
+
21
+ HEADER_KEY = 'Cache-Control'.freeze
22
+
23
+ READ_TIMEOUT = 10
24
+ CONNECT_TIMEOUT = 10
25
+
26
+ UPDATER_BLOCK = proc do
27
+ loop do
28
+ begin
29
+ timeout = 0
30
+
31
+ conn = Excon.new(@@source, connect_timeout: @@connect_timeout, read_timeout: @@read_timeout)
32
+ resp = conn.get
33
+
34
+ raise 'Unable to update keys: ' + resp.data[:status_line] unless resp.status == 200
35
+
36
+ if resp.headers.key?(HEADER_KEY) && resp.headers[HEADER_KEY].is_a?(String)
37
+ cache_control = resp.headers[HEADER_KEY].split(/,\s*/)
38
+ h = cache_control.map { |x| /\Amax-age=(?<timeout>\d+)\z/ =~ x; timeout }.compact
39
+ timeout = h.first.to_i if h.length == 1
40
+ else
41
+ log(:warning, "Missing/malformed #{HEADER_KEY} header")
42
+ end
43
+
44
+ if timeout.nil? || !timeout.positive?
45
+ log(:warning, 'Cache timeout was not parsed, falling back to 1 day')
46
+ timeout = 86400
47
+ end
48
+
49
+ # Fallback to longer cache if less than 10 minutes
50
+ if timeout < MIN_CACHE_TIMEOUT
51
+ log(:warning, "Cache timeout less than 10 minutes (#{timeout}s), defaulting to #{MIN_CACHE_TIMEOUT / 60} minutes")
52
+ timeout = MIN_CACHE_TIMEOUT
53
+ end
54
+
55
+ # Fallback to shorter cache if longer than 30 days
56
+ if timeout > MAX_CACHE_TIMEOUT
57
+ log(:warning, "Cache timeout more than a month (#{timeout}s), defaulting to #{MAX_CACHE_TIMEOUT / (24 * 3600)} days")
58
+ timeout = MAX_CACHE_TIMEOUT
59
+ end
60
+
61
+ Thread.current.thread_variable_set('keys', resp.body)
62
+
63
+ # Supposedly recommended by Tink library
64
+ sleep_time = timeout / 2
65
+
66
+ log(:info, "Updated Google signing keys. Sleeping for #{seconds_to_time(sleep_time)}")
67
+
68
+ sleep sleep_time
69
+ rescue Interrupt => e
70
+ # When interrupted
71
+ log(:fatal, 'Quitting: ' + e.message)
72
+ return
73
+ rescue => e
74
+ log(:error, "Exception updating Google signing keys: '#{e.message}' at #{e.backtrace}")
75
+
76
+ # Don't retry excessively.
77
+ sleep 1
78
+ end
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Start a thread that keeps the Google signing keys updated.
84
+ def self.start(logger: nil, source: nil, waittime: BOOT_WAIT_TIME_MAX)
85
+ @@key_updater_semaphore.synchronize do
86
+ # Another thread might have been waiting for on the mutex
87
+ break unless @@key_updater_thread.nil?
88
+
89
+ @@logger = logger
90
+ @@source = if source
91
+ source
92
+ elsif ENV['ENVIRONMENT'] == 'production'
93
+ SIGNING_KEY_URL
94
+ else
95
+ TEST_SIGNING_KEY_URL
96
+ end
97
+ @@read_timeout = READ_TIMEOUT
98
+ @@connect_timeout = CONNECT_TIMEOUT
99
+
100
+ new_thread = Thread.new(&UPDATER_BLOCK)
101
+
102
+ start_time = Time.now.to_i
103
+
104
+ while new_thread.thread_variable_get('keys').nil? && Time.now.to_i - start_time < waittime
105
+ sleep 0.2
106
+ end
107
+ # Body has now been set.
108
+ # Let other clients through.
109
+ @@key_updater_thread = new_thread
110
+
111
+ nil
112
+ end
113
+ end
114
+
115
+ def self.set_timeout(read_timeout: nil, connect_timeout: nil)
116
+ @@read_timeout = read_timeout if read_timeout
117
+ @@connect_timeout = connect_timeout if connect_timeout
118
+ end
119
+
120
+ def self.signing_keys
121
+ start if @@key_updater_thread.nil?
122
+
123
+ @@key_updater_thread.thread_variable_get('keys')
124
+ end
125
+
126
+ # Required for specs
127
+ def self.terminate
128
+ @@key_updater_thread&.terminate
129
+ @@key_updater_thread = nil
130
+ end
131
+
132
+ class << self
133
+ private
134
+
135
+ def log(level, message)
136
+ return if @@logger.nil?
137
+
138
+ case level
139
+ when :info
140
+ @@logger.info(message)
141
+ when :warning
142
+ if @@logger.respond_to?(:warning)
143
+ @@logger.warning(message)
144
+ elsif @@logger.respond_to?(:warn)
145
+ @@logger.warn(message)
146
+ end
147
+ when :error
148
+ @@logger.error(message)
149
+ when :fatal
150
+ @@logger.fatal(message)
151
+ else
152
+ throw RuntimeError.new('Invalid log level')
153
+ end
154
+ end
155
+
156
+ def seconds_to_time(s)
157
+ days = (s / 86400).floor
158
+ s %= 86400
159
+
160
+ hours = (s / 3600).floor
161
+ s %= 3600
162
+
163
+ minutes = (s / 60).floor
164
+ s %= 60
165
+
166
+ out = ''
167
+ out += "#{days} days " if days.positive?
168
+ out += "#{hours} hours " if hours.positive?
169
+ out += "#{minutes} minutes " if minutes.positive?
170
+ out += "#{s} seconds" if s.positive?
171
+
172
+ out.rstrip
173
+ end
174
+ end
175
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gsk_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Clearhaus
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: excon
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ description:
42
+ email: hello@clearhaus.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/gsk_cache.rb
48
+ homepage: https://github.com/clearhaus/gsk_cache
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 2.7.7
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Fetches and caches Google Pay signing keys
72
+ test_files: []