clickwrap 0.0.0 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- metadata +311 -16
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems/version"
|
|
4
|
+
|
|
5
|
+
module Clickwrap
|
|
6
|
+
module IpGeolocation
|
|
7
|
+
# The optional official adapter for the `trackdown` gem.
|
|
8
|
+
#
|
|
9
|
+
# bundle add trackdown --version ">= 0.4"
|
|
10
|
+
#
|
|
11
|
+
# Clickwrap.configure do |config|
|
|
12
|
+
# config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# `trackdown` is NOT a dependency of this gem and must never become one. It
|
|
16
|
+
# is required lazily, inside the constructor, so that a host who never names
|
|
17
|
+
# this class never loads it — and so that a host who does name it without
|
|
18
|
+
# installing it gets one sentence telling them what to do, at the line in
|
|
19
|
+
# their initializer that asked for it, instead of a NameError somewhere in
|
|
20
|
+
# the middle of a capture.
|
|
21
|
+
#
|
|
22
|
+
# Two rules shape the mapping below, and both exist because this adapter
|
|
23
|
+
# sits between a general-purpose geolocation gem and an evidence record that
|
|
24
|
+
# has to still be readable and honest in several years.
|
|
25
|
+
#
|
|
26
|
+
# ONE: every field is read through `respond_to?`. Trackdown's result object
|
|
27
|
+
# has gained fields across releases and will gain more; a `NoMethodError`
|
|
28
|
+
# during a capture would roll back the protected action for a field the
|
|
29
|
+
# policy may not even have authorized. Reading defensively means a newer
|
|
30
|
+
# Trackdown supplying an accuracy radius is picked up here with no change,
|
|
31
|
+
# and an older one simply reports nil.
|
|
32
|
+
#
|
|
33
|
+
# TWO: `to_h` is never persisted. Trackdown's `to_h` includes `country_info`
|
|
34
|
+
# — the whole ISO3166 country record — and a general gem is right to offer
|
|
35
|
+
# it. Copying it into evidence would store data no policy authorized and no
|
|
36
|
+
# receipt could explain. Clickwrap copies named fields, one at a time, and
|
|
37
|
+
# the extractor then keeps only the subset the server-owned policy allowed.
|
|
38
|
+
#
|
|
39
|
+
# Pinned sources for the mapping (released tag, not a moving branch):
|
|
40
|
+
# result object -> https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/location_result.rb
|
|
41
|
+
# Cloudflare -> https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/cloudflare_provider.rb
|
|
42
|
+
# MaxMind -> https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/maxmind_provider.rb
|
|
43
|
+
# Trackdown 0.4.0 closed the provenance gap scoped in
|
|
44
|
+
# https://github.com/rameerez/trackdown/issues/8. This adapter requires that
|
|
45
|
+
# release rather than silently manufacturing the missing facts itself.
|
|
46
|
+
class TrackdownResolver < Resolver
|
|
47
|
+
PROVIDER_NAME = "trackdown"
|
|
48
|
+
MINIMUM_TRACKDOWN_VERSION = Gem::Version.new("0.4.0")
|
|
49
|
+
|
|
50
|
+
# Trackdown returns the string "Unknown" for a country or city it could
|
|
51
|
+
# not determine, and Cloudflare's own "no country" code is "XX"
|
|
52
|
+
# (pinned provider source above). Neither is a place. Written into a
|
|
53
|
+
# receipt they would be indistinguishable from a country a provider
|
|
54
|
+
# actually reported, so they are mapped back to nil and the extractor
|
|
55
|
+
# records "the provider supplied no authorized field" instead. This is the
|
|
56
|
+
# single most important line in this file.
|
|
57
|
+
PLACEHOLDER_VALUES = ["unknown", "n/a", "xx", "-"].freeze
|
|
58
|
+
|
|
59
|
+
# What Trackdown could supply at all, by Clickwrap field name. Read
|
|
60
|
+
# dynamically from the installed result object where possible, so a
|
|
61
|
+
# Trackdown release that adds accuracy radius starts reporting the
|
|
62
|
+
# capability without an edit here.
|
|
63
|
+
FIELD_READERS = {
|
|
64
|
+
country: %i[country_code country_name],
|
|
65
|
+
region: %i[region region_name region_code],
|
|
66
|
+
city: %i[city city_name],
|
|
67
|
+
postal_code: %i[postal_code],
|
|
68
|
+
latitude_and_longitude: %i[latitude longitude],
|
|
69
|
+
timezone: %i[timezone time_zone],
|
|
70
|
+
continent: %i[continent continent_code],
|
|
71
|
+
metro_code: %i[metro_code],
|
|
72
|
+
accuracy_radius_in_kilometers: %i[accuracy_radius accuracy_radius_in_kilometers]
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
# What Trackdown 0.4 can supply across its providers, used only when the
|
|
76
|
+
# result class cannot be inspected. Cloudflare does not supply an accuracy
|
|
77
|
+
# radius, while MaxMind does; with Trackdown's default :auto provider the
|
|
78
|
+
# adapter can therefore supply it even though any one lookup may not.
|
|
79
|
+
PINNED_CAPABILITIES = %i[
|
|
80
|
+
country region city postal_code latitude_and_longitude timezone continent metro_code
|
|
81
|
+
accuracy_radius_in_kilometers
|
|
82
|
+
].freeze
|
|
83
|
+
|
|
84
|
+
attr_reader :capabilities
|
|
85
|
+
|
|
86
|
+
# Trust is per request in Trackdown 0.4. A host registers its verifier with
|
|
87
|
+
# Trackdown, Trackdown runs it against the same request that supplied the
|
|
88
|
+
# CDN headers, and this adapter copies the result's explicit trust state.
|
|
89
|
+
# A constructor-wide boolean would overclaim every request after one
|
|
90
|
+
# deployment assertion, so the old experimental option is refused by name.
|
|
91
|
+
def initialize(provider_source: nil, source_verified_by_host: nil)
|
|
92
|
+
super()
|
|
93
|
+
require "trackdown" unless defined?(::Trackdown)
|
|
94
|
+
ensure_supported_trackdown_version!
|
|
95
|
+
|
|
96
|
+
unless source_verified_by_host.nil?
|
|
97
|
+
raise ConfigurationError,
|
|
98
|
+
"TrackdownResolver no longer accepts `source_verified_by_host:`. Trackdown " \
|
|
99
|
+
"0.4 verifies source trust per request. Configure " \
|
|
100
|
+
"`Trackdown.configuration.verify_request_came_through_trusted_cloudflare_path_with` " \
|
|
101
|
+
"or the matching CloudFront helper; the resolver will record the result's " \
|
|
102
|
+
"`source_was_verified_by_host?` value."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@provider_source_fallback = (provider_source || configured_provider_source).to_s
|
|
106
|
+
@capabilities = detect_capabilities.freeze
|
|
107
|
+
|
|
108
|
+
freeze
|
|
109
|
+
rescue ::LoadError => error
|
|
110
|
+
raise ConfigurationError,
|
|
111
|
+
"Clickwrap::IpGeolocation::TrackdownResolver needs the `trackdown` gem, which is " \
|
|
112
|
+
"not installed. Run `bundle add trackdown --version \">= 0.4\"` and configure " \
|
|
113
|
+
"it (it needs either a " \
|
|
114
|
+
"MaxMind database or Cloudflare visitor-location headers), or set " \
|
|
115
|
+
"`config.ip_geolocation_resolver = nil` and turn off the IP-geolocation fields " \
|
|
116
|
+
"your policies enable. The underlying load error was: #{error.message}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def resolve(ip_address, http_request: nil)
|
|
120
|
+
address = ip_address.to_s.strip
|
|
121
|
+
return unavailable("no_ip_address_to_resolve") if address.empty?
|
|
122
|
+
|
|
123
|
+
result = ::Trackdown.locate(address, request: http_request)
|
|
124
|
+
return unavailable("provider_returned_no_result") if result.nil?
|
|
125
|
+
|
|
126
|
+
if result.respond_to?(:unavailable?) && result.unavailable?
|
|
127
|
+
return unavailable(
|
|
128
|
+
text(result, :unavailable_reason) || "provider_returned_unavailable",
|
|
129
|
+
result:
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
location = build_location(result)
|
|
134
|
+
|
|
135
|
+
# Trackdown answers with a result object full of "Unknown" when no
|
|
136
|
+
# provider is configured, when its database has no row for the address,
|
|
137
|
+
# and when a Cloudflare country header says "XX". Once the placeholders
|
|
138
|
+
# are mapped away that is an empty answer, and an empty answer is an
|
|
139
|
+
# unavailable result with a reason on it — not a location whose every
|
|
140
|
+
# field happens to be blank.
|
|
141
|
+
return unavailable("provider_supplied_no_location_fields", result:) unless location.any_data_field?
|
|
142
|
+
|
|
143
|
+
location
|
|
144
|
+
rescue StandardError => error
|
|
145
|
+
# Trackdown raises for ordinary conditions — a private or loopback
|
|
146
|
+
# address in development, a missing MaxMind database, a lookup timeout.
|
|
147
|
+
# None of those should abort a capture by itself: the policy decides
|
|
148
|
+
# whether unavailable IP geolocation is fatal. The reason carries the
|
|
149
|
+
# error CLASS and never the message, because a provider message can
|
|
150
|
+
# quote the IP address and this string is written to a column that a
|
|
151
|
+
# redacted receipt is allowed to show.
|
|
152
|
+
unavailable("trackdown_raised_#{error.class}")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def unavailable(reason, result: nil)
|
|
158
|
+
Location.unavailable(
|
|
159
|
+
reason:,
|
|
160
|
+
provider_name: provider_name(result),
|
|
161
|
+
provider_source: provider_source(result),
|
|
162
|
+
database_version: database_version(result),
|
|
163
|
+
database_sha256: database_sha256(result),
|
|
164
|
+
estimated: true,
|
|
165
|
+
source_was_verified_by_host: source_was_verified_by_host?(result),
|
|
166
|
+
resolved_at: resolved_at(result)
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def build_location(result)
|
|
171
|
+
Location.new(
|
|
172
|
+
country_code: text(result, :country_code),
|
|
173
|
+
country_name: text(result, :country_name),
|
|
174
|
+
region_name: text(result, :region, :region_name),
|
|
175
|
+
region_code: text(result, :region_code),
|
|
176
|
+
city_name: text(result, :city, :city_name),
|
|
177
|
+
postal_code: text(result, :postal_code),
|
|
178
|
+
latitude: number(result, :latitude),
|
|
179
|
+
longitude: number(result, :longitude),
|
|
180
|
+
timezone: text(result, :timezone, :time_zone),
|
|
181
|
+
# Both Trackdown providers put a two-letter code in `continent`
|
|
182
|
+
# (MaxMind's `continent.code`, Cloudflare's `CF-IPContinent`), which
|
|
183
|
+
# is why it maps to the continent CODE column rather than a name.
|
|
184
|
+
continent_code: text(result, :continent, :continent_code),
|
|
185
|
+
metro_code: text(result, :metro_code),
|
|
186
|
+
provider_name: provider_name(result),
|
|
187
|
+
provider_source: provider_source(result),
|
|
188
|
+
database_version: database_version(result),
|
|
189
|
+
database_sha256: database_sha256(result),
|
|
190
|
+
accuracy_radius_in_kilometers: number(result, :accuracy_radius_in_kilometers, :accuracy_radius),
|
|
191
|
+
accuracy_radius_confidence_percentage: number(result, :accuracy_radius_confidence_percentage),
|
|
192
|
+
# Always. No IP geolocation result from any provider is an
|
|
193
|
+
# observation of where anyone was; it is an estimate about an address,
|
|
194
|
+
# and this flag is what keeps a receipt from implying otherwise.
|
|
195
|
+
estimated: true,
|
|
196
|
+
source_was_verified_by_host: source_was_verified_by_host?(result),
|
|
197
|
+
resolved_at: resolved_at(result)
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Reads the first reader the result object actually has. Returns nil for a
|
|
202
|
+
# missing reader, a blank value, or one of Trackdown's placeholders — a
|
|
203
|
+
# missing field must be indistinguishable from nothing, never from an
|
|
204
|
+
# answer.
|
|
205
|
+
def text(result, *readers)
|
|
206
|
+
value = first_value(result, readers)
|
|
207
|
+
return nil if value.nil?
|
|
208
|
+
|
|
209
|
+
string = value.to_s.strip
|
|
210
|
+
return nil if string.empty?
|
|
211
|
+
return nil if PLACEHOLDER_VALUES.include?(string.downcase)
|
|
212
|
+
|
|
213
|
+
string
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def number(result, *readers)
|
|
217
|
+
value = first_value(result, readers)
|
|
218
|
+
return nil if value.nil?
|
|
219
|
+
return nil if value.to_s.strip.empty?
|
|
220
|
+
|
|
221
|
+
value
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def first_value(result, readers)
|
|
225
|
+
readers.each do |reader|
|
|
226
|
+
next unless result.respond_to?(reader)
|
|
227
|
+
|
|
228
|
+
value = result.public_send(reader)
|
|
229
|
+
return value unless value.nil?
|
|
230
|
+
rescue StandardError
|
|
231
|
+
# A reader that raises is treated as a reader the provider does not
|
|
232
|
+
# have. One awkward field must not cost the whole result.
|
|
233
|
+
next
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def ensure_supported_trackdown_version!
|
|
240
|
+
version = Gem::Version.new(::Trackdown::VERSION.to_s) if defined?(::Trackdown::VERSION)
|
|
241
|
+
return if version && version >= MINIMUM_TRACKDOWN_VERSION
|
|
242
|
+
|
|
243
|
+
installed = version ? version.to_s : "unknown"
|
|
244
|
+
raise ConfigurationError,
|
|
245
|
+
"Clickwrap::IpGeolocation::TrackdownResolver requires trackdown >= " \
|
|
246
|
+
"#{MINIMUM_TRACKDOWN_VERSION}; the loaded version is #{installed}. Run " \
|
|
247
|
+
"`bundle update trackdown` before enabling this resolver. Earlier releases do " \
|
|
248
|
+
"not expose the per-request source trust and provider provenance Clickwrap " \
|
|
249
|
+
"would otherwise have to guess."
|
|
250
|
+
rescue ArgumentError
|
|
251
|
+
raise ConfigurationError,
|
|
252
|
+
"Clickwrap::IpGeolocation::TrackdownResolver could not parse the loaded " \
|
|
253
|
+
"Trackdown::VERSION (#{::Trackdown::VERSION.inspect}). Install trackdown >= " \
|
|
254
|
+
"#{MINIMUM_TRACKDOWN_VERSION} before enabling this resolver."
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def provider_name(result)
|
|
258
|
+
text(result, :provider_name, :provider) || PROVIDER_NAME
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def provider_source(result)
|
|
262
|
+
text(result, :provider_source) || @provider_source_fallback
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Trackdown exposes MaxMind's exact database build epoch rather than a
|
|
266
|
+
# marketing-style version label. Preserve that integer in a
|
|
267
|
+
# self-describing string: formatting it as a date would throw away the
|
|
268
|
+
# fact that it came from the database metadata and could create timezone
|
|
269
|
+
# ambiguity years later.
|
|
270
|
+
def database_version(result)
|
|
271
|
+
legacy_version = text(result, :database_version, :database_build_version)
|
|
272
|
+
return legacy_version if legacy_version
|
|
273
|
+
|
|
274
|
+
epoch = first_value(result, %i[database_build_epoch])
|
|
275
|
+
return nil unless epoch.is_a?(Numeric)
|
|
276
|
+
|
|
277
|
+
"database_build_epoch:#{epoch.to_i}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def database_sha256(result)
|
|
281
|
+
digest = text(result, :database_sha256, :database_digest)
|
|
282
|
+
return nil if digest.nil?
|
|
283
|
+
return digest if digest.match?(/\Asha256:[0-9a-f]{64}\z/i)
|
|
284
|
+
return "sha256:#{digest.downcase}" if digest.match?(/\A[0-9a-f]{64}\z/i)
|
|
285
|
+
|
|
286
|
+
# Do not relabel a provider's non-SHA value as SHA-256. Retaining it is
|
|
287
|
+
# more honest than inventing an algorithm; a host can still inspect the
|
|
288
|
+
# upstream value and a verifier will not mistake it for our digest form.
|
|
289
|
+
digest
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def source_was_verified_by_host?(result)
|
|
293
|
+
result.respond_to?(:source_was_verified_by_host?) &&
|
|
294
|
+
result.source_was_verified_by_host? == true
|
|
295
|
+
rescue StandardError
|
|
296
|
+
false
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def resolved_at(result)
|
|
300
|
+
first_value(result, %i[resolved_at]) || Clickwrap.now
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Fallback provenance for failures that occur before Trackdown can return
|
|
304
|
+
# a result. Successful Trackdown 0.4 results name the provider that
|
|
305
|
+
# actually answered, including when configuration.provider is `:auto`.
|
|
306
|
+
def configured_provider_source
|
|
307
|
+
return "unspecified" unless ::Trackdown.respond_to?(:configuration)
|
|
308
|
+
|
|
309
|
+
::Trackdown.configuration.provider.to_s
|
|
310
|
+
rescue StandardError
|
|
311
|
+
"unspecified"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def detect_capabilities
|
|
315
|
+
result_class = trackdown_result_class
|
|
316
|
+
return PINNED_CAPABILITIES.dup if result_class.nil?
|
|
317
|
+
|
|
318
|
+
supplied = FIELD_READERS.select do |_field, readers|
|
|
319
|
+
readers.any? { |reader| result_class.method_defined?(reader) }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
supplied.keys
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def trackdown_result_class
|
|
326
|
+
defined?(::Trackdown::LocationResult) ? ::Trackdown::LocationResult : nil
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Built-in resolver value objects and adapters are part of Clickwrap's public
|
|
4
|
+
# initializer API, so they must be available before Rails finishes setting up
|
|
5
|
+
# Zeitwerk. In particular, a host should be able to write the documented line
|
|
6
|
+
#
|
|
7
|
+
# config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
|
|
8
|
+
#
|
|
9
|
+
# without knowing a private file path or adding a load-order `require`. The
|
|
10
|
+
# Trackdown adapter itself still loads the optional `trackdown` gem lazily in
|
|
11
|
+
# its constructor; requiring Clickwrap never makes Trackdown a dependency.
|
|
12
|
+
require_relative "ip_geolocation/location"
|
|
13
|
+
require_relative "ip_geolocation/resolver"
|
|
14
|
+
require_relative "ip_geolocation/null_resolver"
|
|
15
|
+
require_relative "ip_geolocation/static_resolver"
|
|
16
|
+
require_relative "ip_geolocation/trackdown_resolver"
|