Class: Otto::Privacy::RedactedFingerprint

Inherits:
Object
  • Object
show all
Defined in:
lib/otto/privacy/redacted_fingerprint.rb

Overview

Immutable privacy-safe request fingerprint (aka CrappyFingerprint)

Contains anonymized information about a request that can be used for logging, analytics, and session tracking without storing personally identifiable information.

Examples:

Create from Rack environment

config = Otto::Privacy::Config.new
fingerprint = RedactedFingerprint.new(env, config)
fingerprint.masked_ip   # => '192.168.1.0'
fingerprint.country     # => 'US'

Constant Summary collapse

GEO_MASKED_FORWARDED_HEADERS =

IP-bearing forwarded headers overwritten with the masked IP in the geo-resolution env view. Mirrors the set IPPrivacyMiddleware#mask_forwarded_headers rewrites, so a custom resolver reading env sees masked values everywhere the middleware would. The structured RFC 7239 Forwarded header (HTTP_FORWARDED) is handled separately in #geo_env (dropped, not swapped, to keep valid syntax).

%w[
  HTTP_X_FORWARDED_FOR
  HTTP_X_REAL_IP
  HTTP_X_CLIENT_IP
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env, config, geo_headers_trusted: true) ⇒ RedactedFingerprint

Create a new RedactedFingerprint from a Rack environment

Parameters:

  • env (Hash)

    Rack environment hash

  • config (Otto::Privacy::Config)

    Privacy configuration

  • geo_headers_trusted (Boolean) (defaults to: true)

    whether request geo headers may be trusted for this request. The middleware passes false for a non-trusted-proxy request when trusted proxies are configured, so spoofed geo headers are ignored. Defaults to true for standalone use.



48
49
50
51
52
53
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
79
80
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 48

def initialize(env, config, geo_headers_trusted: true)
  remote_ip = env['REMOTE_ADDR']

  @session_id = SecureRandom.uuid
  @timestamp = Time.now.utc
  @masked_ip = IPPrivacy.mask_ip(remote_ip, config.octet_precision)
  @hashed_ip = IPPrivacy.hash_ip(remote_ip, config.rotation_key)
  # hashed_ip is computed above from the real IP; geo resolution then runs
  # against a MASKED view — the masked IP AND an env with the IP-bearing
  # headers masked — so neither a custom resolver nor the database can see
  # the unmasked address, via the argument or via env. Country-level
  # networks are >= /24, so the /24-masked IP resolves to the same country.
  @country = if config.geo_enabled
               GeoResolver.resolve(@masked_ip, geo_env(env), config, headers_trusted: geo_headers_trusted)
             end
  # ASN keeps the masked-IP contract: IPv4 routes are not announced longer
  # than /24, so a masked address falls in the same announced prefix and
  # therefore the same ASN.
  @asn = AsnResolver.resolve(@masked_ip, config) if config.asn_enabled
  # The anonymizer is the one lookup that gets the REAL address, for the
  # same reason hashed_ip above does: masking would destroy the answer.
  # Anonymizer databases list individual egress nodes at or near /32, so a
  # /24-masked lookup would report on the node's neighbours instead of the
  # node. Only the resulting label is retained — the address goes no
  # further than the resolver, exactly as it goes no further than hash_ip.
  @anonymizer = AnonymizerResolver.resolve(remote_ip, config) if config.anonymizer_enabled
  @anonymized_ua = anonymize_user_agent(env['HTTP_USER_AGENT'])
  @request_path = env['PATH_INFO']
  @request_method = env['REQUEST_METHOD']
  @referer = anonymize_referer(env['HTTP_REFERER'])

  freeze
end

Instance Attribute Details

#anonymized_uaObject (readonly)

Returns the value of attribute anonymized_ua.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def anonymized_ua
  @anonymized_ua
end

#anonymizerObject (readonly)

Returns the value of attribute anonymizer.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def anonymizer
  @anonymizer
end

#asnObject (readonly)

Returns the value of attribute asn.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def asn
  @asn
end

#countryObject (readonly)

Returns the value of attribute country.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def country
  @country
end

#hashed_ipObject (readonly)

Returns the value of attribute hashed_ip.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def hashed_ip
  @hashed_ip
end

#masked_ipObject (readonly)

Returns the value of attribute masked_ip.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def masked_ip
  @masked_ip
end

#refererObject (readonly)

Returns the value of attribute referer.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def referer
  @referer
end

#request_methodObject (readonly)

Returns the value of attribute request_method.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def request_method
  @request_method
end

#request_pathObject (readonly)

Returns the value of attribute request_path.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def request_path
  @request_path
end

#session_idObject (readonly)

Returns the value of attribute session_id.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def session_id
  @session_id
end

#timestampObject (readonly)

Returns the value of attribute timestamp.



24
25
26
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 24

def timestamp
  @timestamp
end

Instance Method Details

#inspectString

Inspect representation

Returns:

  • (String)

    Detailed representation for debugging



119
120
121
122
123
124
125
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 119

def inspect
  '#<Otto::Privacy::RedactedFingerprint ' \
    "masked_ip=#{@masked_ip.inspect} " \
    "hashed_ip=#{@hashed_ip[0..15]}... " \
    "country=#{@country.inspect} " \
    "timestamp=#{@timestamp.inspect}>"
end

#to_hHash

Convert to hash for logging or serialization

Returns:

  • (Hash)

    Hash representation of fingerprint



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 85

def to_h
  {
        session_id: @session_id,
         timestamp: @timestamp.iso8601,
         masked_ip: @masked_ip,
         hashed_ip: @hashed_ip,
           country: @country,
               asn: @asn,
        anonymizer: @anonymizer,
     anonymized_ua: @anonymized_ua,
    request_method: @request_method,
      request_path: @request_path,
           referer: @referer,
  }
end

#to_json(*_args) ⇒ String

Convert to JSON string

Returns:

  • (String)

    JSON representation



104
105
106
107
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 104

def to_json(*_args)
  require 'json'
  to_h.to_json
end

#to_sString

String representation

Returns:

  • (String)

    Human-readable representation



112
113
114
# File 'lib/otto/privacy/redacted_fingerprint.rb', line 112

def to_s
  "#<RedactedFingerprint #{@hashed_ip[0..15]}... #{@country} #{@timestamp}>"
end