Module: GSKCache

Defined in:
lib/gsk_cache.rb

Constant Summary collapse

SIGNING_KEY_URL =

Keys used for signing in production

'https://payments.developers.google.com/paymentmethodtoken/keys.json'.freeze
TEST_SIGNING_KEY_URL =

Keys used for signing in a testing environment

'https://payments.developers.google.com/paymentmethodtoken/test/keys.json'.freeze
MAX_CACHE_TIMEOUT =

One month

31 * 24 * 3600
MIN_CACHE_TIMEOUT =

10 minutes

600
BOOT_WAIT_TIME_MAX =
8
HEADER_KEY =
'Cache-Control'.freeze
READ_TIMEOUT =
10
CONNECT_TIMEOUT =
10
UPDATER_BLOCK =
proc do
  loop do
    begin
      timeout = 0

      conn = Excon.new(@@source, connect_timeout: @@connect_timeout, read_timeout: @@read_timeout)
      resp = conn.get

      raise 'Unable to update keys: ' + resp.data[:status_line] unless resp.status == 200

      if resp.headers.key?(HEADER_KEY) && resp.headers[HEADER_KEY].is_a?(String)
        cache_control = resp.headers[HEADER_KEY].split(/,\s*/)
        h = cache_control.map { |x| /\Amax-age=(?<timeout>\d+)\z/ =~ x; timeout }.compact
        timeout = h.first.to_i if h.length == 1
      else
        log(:warning, "Missing/malformed #{HEADER_KEY} header")
      end

      if timeout.nil? || !timeout.positive?
        log(:warning, 'Cache timeout was not parsed, falling back to 1 day')
        timeout = 86400
      end

      # Fallback to longer cache if less than 10 minutes
      if timeout < MIN_CACHE_TIMEOUT
        log(:warning, "Cache timeout less than 10 minutes (#{timeout}s), defaulting to #{MIN_CACHE_TIMEOUT / 60} minutes")
        timeout = MIN_CACHE_TIMEOUT
      end

      # Fallback to shorter cache if longer than 30 days
      if timeout > MAX_CACHE_TIMEOUT
        log(:warning, "Cache timeout more than a month (#{timeout}s), defaulting to #{MAX_CACHE_TIMEOUT / (24 * 3600)} days")
        timeout = MAX_CACHE_TIMEOUT
      end

      Thread.current.thread_variable_set('keys', resp.body)

      # Supposedly recommended by Tink library
      sleep_time = timeout / 2

      log(:info, "Updated Google signing keys. Sleeping for #{seconds_to_time(sleep_time)}")

      sleep sleep_time
    rescue Interrupt => e
      # When interrupted
      log(:fatal, 'Quitting: ' + e.message)
      return
    rescue => e
      log(:error, "Exception updating Google signing keys: '#{e.message}' at #{e.backtrace}")

      # Don't retry excessively.
      sleep 1
    end
  end
end
@@key_updater_semaphore =
Mutex.new
@@key_updater_thread =
nil

Class Method Summary collapse

Class Method Details

.set_timeout(read_timeout: nil, connect_timeout: nil) ⇒ Object



115
116
117
118
# File 'lib/gsk_cache.rb', line 115

def self.set_timeout(read_timeout: nil, connect_timeout: nil)
  @@read_timeout    = read_timeout    if read_timeout
  @@connect_timeout = connect_timeout if connect_timeout
end

.signing_keysObject



120
121
122
123
124
# File 'lib/gsk_cache.rb', line 120

def self.signing_keys
  start if @@key_updater_thread.nil?

  @@key_updater_thread.thread_variable_get('keys')
end

.start(logger: nil, source: nil, waittime: BOOT_WAIT_TIME_MAX) ⇒ Object

Start a thread that keeps the Google signing keys updated.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/gsk_cache.rb', line 84

def self.start(logger: nil, source: nil, waittime: BOOT_WAIT_TIME_MAX)
  @@key_updater_semaphore.synchronize do
    # Another thread might have been waiting for on the mutex
    break unless @@key_updater_thread.nil?

    @@logger = logger
    @@source = if source
                 source
               elsif ENV['ENVIRONMENT'] == 'production'
                 SIGNING_KEY_URL
               else
                 TEST_SIGNING_KEY_URL
               end
    @@read_timeout    = READ_TIMEOUT
    @@connect_timeout = CONNECT_TIMEOUT

    new_thread = Thread.new(&UPDATER_BLOCK)

    start_time = Time.now.to_i

    while new_thread.thread_variable_get('keys').nil? && Time.now.to_i - start_time < waittime
      sleep 0.2
    end
    # Body has now been set.
    # Let other clients through.
    @@key_updater_thread = new_thread

    nil
  end
end

.terminateObject

Required for specs



127
128
129
130
# File 'lib/gsk_cache.rb', line 127

def self.terminate
  @@key_updater_thread&.terminate
  @@key_updater_thread = nil
end