Module: DebugBundle::TriggerToken

Defined in:
lib/debugbundle/trigger_token.rb

Constant Summary collapse

HEADER_NAME =
'x-debugbundle-probe-trigger'
QUERY_PARAMETER_NAME =
'_debug_probe'
TOKEN_PREFIX =
'dbundle_probe_'

Class Method Summary collapse

Class Method Details

.base64url_decode(value) ⇒ Object



108
109
110
111
# File 'lib/debugbundle/trigger_token.rb', line 108

def self.base64url_decode(value)
  decoded = base64url_decode_bytes(value)
  decoded&.force_encoding('UTF-8')
end

.base64url_decode_bytes(value) ⇒ Object



113
114
115
116
117
118
119
120
# File 'lib/debugbundle/trigger_token.rb', line 113

def self.base64url_decode_bytes(value)
  padded = value.dup
  remainder = padded.length % 4
  padded += '=' * (4 - remainder) if remainder.positive?
  Base64.urlsafe_decode64(padded)
rescue ArgumentError
  nil
end

.decode_payload(payload_segment) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/debugbundle/trigger_token.rb', line 54

def self.decode_payload(payload_segment)
  decoded = base64url_decode(payload_segment)
  return nil unless decoded

  parsed = JSON.parse(decoded)
  return nil unless parsed.is_a?(Hash)

  activation_id = parsed['activation_id']
  label_pattern = parsed['label_pattern']
  service = parsed['service']
  environment = parsed['environment']
  expires_at = Time.iso8601(parsed['trigger_expires_at'])

  return nil if [activation_id, label_pattern, service, environment].any? { |value| value.to_s.empty? }

  {
    activation_id: activation_id,
    label_pattern: label_pattern,
    service: service,
    environment: environment,
    expires_at: expires_at
  }
rescue JSON::ParserError, ArgumentError, TypeError
  nil
end

.extract_map_value(mapping, target_key, case_insensitive:) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/debugbundle/trigger_token.rb', line 96

def self.extract_map_value(mapping, target_key, case_insensitive:)
  mapping.each do |key, value|
    matches = case_insensitive ? key.to_s.downcase == target_key.downcase : key.to_s == target_key
    next unless matches

    return value if value.is_a?(String) && !value.empty?
    return value.first if value.is_a?(Array) && value.first.is_a?(String) && !value.first.empty?
  end

  nil
end

.extract_token(request) ⇒ Object



38
39
40
41
42
43
44
45
# File 'lib/debugbundle/trigger_token.rb', line 38

def self.extract_token(request)
  headers = request[:headers] || request['headers'] || {}
  header_token = extract_map_value(headers, HEADER_NAME, case_insensitive: true)
  return header_token if header_token

  query = request[:query] || request['query'] || {}
  extract_map_value(query, QUERY_PARAMETER_NAME, case_insensitive: false)
end

.resolve_request_directives(request:, trigger_token_key:) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/debugbundle/trigger_token.rb', line 13

def self.resolve_request_directives(request:, trigger_token_key:)
  return [] if request.nil? || trigger_token_key.to_s.empty?

  token = extract_token(request)
  return [] unless token&.start_with?(TOKEN_PREFIX)

  payload_segment, signature_segment = split_token(token.delete_prefix(TOKEN_PREFIX))
  return [] unless payload_segment && signature_segment
  return [] unless valid_signature?(payload_segment, signature_segment, trigger_token_key)

  payload = decode_payload(payload_segment)
  return [] unless payload
  return [] if payload[:expires_at] <= Time.now.utc

  [
    RemoteConfig::Directive.new(
      id: payload[:activation_id],
      label_pattern: payload[:label_pattern],
      service: payload[:service],
      environment: payload[:environment],
      expires_at: payload[:expires_at]
    )
  ]
end

.secure_compare(left, right) ⇒ Object



88
89
90
91
92
93
94
# File 'lib/debugbundle/trigger_token.rb', line 88

def self.secure_compare(left, right)
  result = 0
  left.bytes.zip(right.bytes) do |left_byte, right_byte|
    result |= left_byte ^ right_byte
  end
  result.zero?
end

.split_token(token) ⇒ Object



47
48
49
50
51
52
# File 'lib/debugbundle/trigger_token.rb', line 47

def self.split_token(token)
  separator_index = token.index('.')
  return [nil, nil] unless separator_index&.positive? && separator_index < (token.length - 1)

  [token[0...separator_index], token[(separator_index + 1)..]]
end

.valid_signature?(payload_segment, signature_segment, trigger_token_key) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
83
84
85
86
# File 'lib/debugbundle/trigger_token.rb', line 80

def self.valid_signature?(payload_segment, signature_segment, trigger_token_key)
  expected = OpenSSL::HMAC.digest('sha256', trigger_token_key, payload_segment)
  actual = base64url_decode_bytes(signature_segment)
  return false unless actual && actual.bytesize == expected.bytesize

  secure_compare(expected, actual)
end