1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
module ProTacts
# Redacts card content from request bodies on their way to Sentry.
#
# Everything else in a request is fine to send: hrefs carry opaque contact
# UIDs, and the IPs are tailnet addresses. Card content is the exception,
# and a write path would put it straight in a PUT body.
#
# Sentry truncates bodies at 16KB (RequestInterface::MAX_BODY_LIMIT), so a
# card can arrive with its BEGIN and no END. The trailing alternation
# handles that, and is load-bearing for the ordinary case too: (?~) is
# greedy up to the longest run with no complete END:VCARD in it, which
# runs into the marker and stops at END:VCAR. Requiring END:VCARD or the
# end of the body is what backtracks it onto the real boundary.
module SentryScrubber
VCARD = /BEGIN:VCARD(?~END:VCARD)(?:END:VCARD|\z)/mi
VCARD_CONTENT_TYPE = %r{\Atext/vcard}i
REDACTED = "[vcard redacted]".freeze #: String
# Sentry discards the event unless a Sentry::ErrorEvent comes back, so
# this returns the event it was handed either way.
#: (untyped event, ?untyped hint) -> untyped
def self.call(event, _hint = nil)
request = event.request if event.respond_to?(:request)
return event unless request
request.data =
if vcard_body?(request)
REDACTED
else
redact(request.data)
end
event
end
# A card PUT is a card whether or not it parses, so the content type is
# enough on its own.
#: (untyped request) -> bool
def self.vcard_body?(request)
headers = request.headers
return false unless headers.respond_to?(:[])
headers["Content-Type"].to_s.match?(VCARD_CONTENT_TYPE)
end
# Form bodies arrive as a params hash rather than a string.
#: (untyped data) -> untyped
def self.redact(data)
case data
when String then data.gsub(VCARD, REDACTED)
when Hash then data.transform_values { redact(it) }
when Array then data.map { redact(it) }
else data
end
end
end
end