Skip to content

Commit ad1fbfd

Browse files
authored
HTTPEvents: filter headers using http_header_filters configuration (#10)
1 parent 943ded5 commit ad1fbfd

File tree

5 files changed

+93
-10
lines changed

5 files changed

+93
-10
lines changed

.github/workflows/main.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ jobs:
2323
- 2.5
2424
- 2.4
2525
- 2.3
26-
- 2.2
2726
- jruby-9.4.3.0
2827
- jruby-9.2.14.0
2928
- truffleruby-23.0.0

lib/logtail-rack/http_events.rb

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,24 @@ def silence_request
102102
@silence_request
103103
end
104104

105+
# Filter sensitive HTTP headers (such as "Authorization: Bearer secret_token")
106+
#
107+
# Filtered HTTP header values will be sent to Better Stack as "[FILTERED]"
108+
#
109+
# @example
110+
# Logtail::Integrations::Rack::HTTPEvents.http_header_filters = ["Authorization"]
105111
def http_header_filters=(value)
106-
@http_header_filters = value
112+
@http_header_filters = value.map { |header_name| normalize_header_name(header_name) }
107113
end
108114

109115
# Accessor method for {#http_header_filters=}
110116
def http_header_filters
111117
@http_header_filters
112118
end
119+
120+
def normalize_header_name(name)
121+
name.to_s.downcase.gsub("-", "_")
122+
end
113123
end
114124

115125
CONTENT_LENGTH_KEY = 'Content-Length'.freeze
@@ -138,12 +148,11 @@ def call(env)
138148

139149
http_response = HTTPResponse.new(
140150
content_length: content_length,
141-
headers: headers,
151+
headers: filter_http_headers(headers),
142152
http_context: http_context,
143153
request_id: request.request_id,
144154
status: status,
145155
duration_ms: duration_ms,
146-
headers_to_sanitize: self.class.http_header_filters,
147156
)
148157

149158
{
@@ -169,15 +178,14 @@ def call(env)
169178
http_request = HTTPRequest.new(
170179
body: event_body,
171180
content_length: safe_to_i(request.content_length),
172-
headers: request.headers,
181+
headers: filter_http_headers(request.headers),
173182
host: force_encoding(request.host),
174183
method: request.request_method,
175184
path: request.path,
176185
port: request.port,
177186
query_string: force_encoding(request.query_string),
178187
request_id: request.request_id,
179188
scheme: force_encoding(request.scheme),
180-
headers_to_sanitize: self.class.http_header_filters,
181189
)
182190

183191
{
@@ -212,11 +220,10 @@ def call(env)
212220
http_response = HTTPResponse.new(
213221
body: event_body,
214222
content_length: content_length,
215-
headers: headers,
223+
headers: filter_http_headers(headers),
216224
request_id: request.request_id,
217225
status: status,
218226
duration_ms: duration_ms,
219-
headers_to_sanitize: self.class.http_header_filters,
220227
)
221228

222229
{
@@ -260,6 +267,13 @@ def silenced?(env, request)
260267
end
261268
end
262269

270+
def filter_http_headers(headers)
271+
headers.each do |name, _|
272+
normalized_header_name = self.class.normalize_header_name(name)
273+
headers[name] = "[FILTERED]" if self.class.http_header_filters&.include?(normalized_header_name)
274+
end
275+
end
276+
263277
def safe_to_i(val)
264278
val.nil? ? nil : val.to_i
265279
end

lib/logtail-rack/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Logtail
22
module Integrations
33
module Rack
4-
VERSION = "0.2.1"
4+
VERSION = "0.2.2"
55
end
66
end
77
end

logtail-ruby-rack.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
1212
spec.homepage = "https://github.com/logtail/logtail-ruby-rack"
1313
spec.license = "ISC"
1414

15-
spec.required_ruby_version = '>= 2.2.10'
15+
spec.required_ruby_version = '>= 2.3'
1616

1717
spec.metadata["homepage_uri"] = spec.homepage
1818
spec.metadata["source_code_uri"] = "https://github.com/logtail/logtail-ruby-rack"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
require "spec_helper"
2+
require "logtail-rack/config"
3+
require 'stringio'
4+
5+
6+
RSpec.describe Logtail::Integrations::Rack::HTTPEvents do
7+
let(:app) { ->(env) { [200, env, "app"] } }
8+
let(:mock_request) { Rack::MockRequest.env_for('https://example.com/test-page', { 'HTTP_AUTHORIZATION' => 'Bearer secret_token', 'HTTP_CONTENT_TYPE' => 'text/plain' }) }
9+
10+
let :middleware do
11+
described_class.new(app)
12+
end
13+
14+
it "log HTTP request and response" do
15+
logs = capture_logs { middleware.call mock_request }
16+
17+
expect(logs.map { |log| log['message'] }).to match(['Started GET "/test-page"', /Completed 200 OK in \d+\.\d+ms/])
18+
end
19+
20+
it "log HTTP request headers" do
21+
logs = capture_logs { middleware.call mock_request }
22+
23+
request_headers_json = logs.first["event"]["http_request_received"]["headers_json"]
24+
expect(JSON.parse(request_headers_json)).to eq({"Authorization" => "Bearer secret_token", "Content_Type" => "text/plain"})
25+
end
26+
27+
it "filter HTTP request headers using http_header_filters" do
28+
logs = capture_logs { with_http_header_filters(%w[Authorization]) { middleware.call mock_request } }
29+
30+
request_headers_json = logs.first["event"]["http_request_received"]["headers_json"]
31+
expect(JSON.parse(request_headers_json)).to eq({"Authorization" => "[FILTERED]", "Content_Type" => "text/plain"})
32+
end
33+
34+
it "filter HTTP request headers using http_header_filters without regard to case or dashes" do
35+
logs = capture_logs { with_http_header_filters(%w[authorization CONTENT-TYPE]) { middleware.call mock_request } }
36+
37+
request_headers_json = logs.first["event"]["http_request_received"]["headers_json"]
38+
expect(JSON.parse(request_headers_json)).to eq({"Authorization" => "[FILTERED]", "Content_Type" => "[FILTERED]"})
39+
end
40+
41+
it "ignores non-existent headers in http_header_filters" do
42+
logs = capture_logs { with_http_header_filters(%w[Not_Found_Header]) { middleware.call mock_request } }
43+
44+
request_headers_json = logs.first["event"]["http_request_received"]["headers_json"]
45+
expect(JSON.parse(request_headers_json)).to eq({"Authorization" => "Bearer secret_token", "Content_Type" => "text/plain"})
46+
end
47+
48+
def capture_logs(&blk)
49+
old_logger = Logtail::Config.instance.logger
50+
51+
string_io = StringIO.new
52+
logger = Logtail::Logger.new(string_io)
53+
logger.formatter = Logtail::Logger::JSONFormatter.new
54+
Logtail::Config.instance.logger = logger
55+
56+
blk.call
57+
58+
string_io.string.split("\n").map { |record| JSON.parse(record) }
59+
ensure
60+
Logtail::Config.instance.logger = old_logger
61+
end
62+
63+
def with_http_header_filters(headers, &blk)
64+
previous_http_header_filters = Logtail::Integrations::Rack::HTTPEvents.http_header_filters = headers
65+
66+
blk.call
67+
ensure
68+
Logtail::Integrations::Rack::HTTPEvents.http_header_filters = previous_http_header_filters
69+
end
70+
end

0 commit comments

Comments
 (0)