gsk_cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []