From 04a6f3b75927e7cf78b895e94e8ff7de2f5cdb82 Mon Sep 17 00:00:00 2001 From: Peter Souter Date: Fri, 13 Jan 2017 10:28:29 +0000 Subject: [PATCH] Added filter method for logger middleware (#650) Allows you to filter sensitive information with a regex, allowing a gsub with redaction. --- README.md | 8 ++++++++ lib/faraday/response/logger.rb | 24 ++++++++++++++++++----- test/adapters/logger_test.rb | 35 +++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ef8e7af1..80264735 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,14 @@ conn = Faraday.new(:url => 'http://sushi.com') do |faraday| faraday.adapter Faraday.default_adapter # make requests with Net::HTTP end +# Filter sensitive information from logs with a regex matcher + +conn = Faraday.new(:url => 'http://sushi.com/api_key=s3cr3t') do |faraday| + faraday.response :logger do | logger | + logger.filter(/(api_key=)(\w+)/,'\1[REMOVED]') + end +end + ## GET ## response = conn.get '/nigiri/sake.json' # GET http://sushi.com/nigiri/sake.json diff --git a/lib/faraday/response/logger.rb b/lib/faraday/response/logger.rb index c86d1dba..06b72a17 100644 --- a/lib/faraday/response/logger.rb +++ b/lib/faraday/response/logger.rb @@ -12,22 +12,28 @@ module Faraday require 'logger' ::Logger.new(STDOUT) end + @filter = [] @options = DEFAULT_OPTIONS.merge(options) + yield self if block_given? end def_delegators :@logger, :debug, :info, :warn, :error, :fatal def call(env) - info "#{env.method} #{env.url.to_s}" - debug('request') { dump_headers env.request_headers } if log_headers?(:request) - debug('request') { dump_body(env[:body]) } if env[:body] && log_body?(:request) + info "#{env.method} #{apply_filters(env.url.to_s)}" + debug('request') { apply_filters( dump_headers env.request_headers ) } if log_headers?(:request) + debug('request') { apply_filters( dump_body(env[:body]) ) } if env[:body] && log_body?(:request) super end def on_complete(env) info('Status') { env.status.to_s } - debug('response') { dump_headers env.response_headers } if log_headers?(:response) - debug('response') { dump_body env[:body] } if env[:body] && log_body?(:response) + debug('response') { apply_filters( dump_headers env.response_headers ) } if log_headers?(:response) + debug('response') { apply_filters( dump_body env[:body] ) } if env[:body] && log_body?(:response) + end + + def filter(filter_word, filter_replacement) + @filter.push([ filter_word, filter_replacement ]) end private @@ -62,5 +68,13 @@ module Faraday else @options[:bodies] end end + + def apply_filters(output) + @filter.each do |pattern, replacement| + output = output.to_s.gsub(pattern, replacement) + end + output + end + end end diff --git a/test/adapters/logger_test.rb b/test/adapters/logger_test.rb index 74a907a0..cddaed31 100644 --- a/test/adapters/logger_test.rb +++ b/test/adapters/logger_test.rb @@ -8,12 +8,20 @@ module Adapters rubbles = ['Barney', 'Betty', 'Bam Bam'] Faraday.new do |b| - b.response :logger, logger, logger_options + b.response :logger, @logger, logger_options do | logger | + logger.filter(/(soylent green is) (.+)/,'\1 tasty') + logger.filter(/(api_key:).*"(.+)."/,'\1[API_KEY]') + logger.filter(/(password)=(.+)/,'\1=[HIDDEN]') + end b.adapter :test do |stubs| stubs.get('/hello') { [200, {'Content-Type' => 'text/html'}, 'hello'] } stubs.post('/ohai') { [200, {'Content-Type' => 'text/html'}, 'fred'] } stubs.post('/ohyes') { [200, {'Content-Type' => 'text/html'}, 'pebbles'] } stubs.get('/rubbles') { [200, {'Content-Type' => 'application/json'}, rubbles] } + stubs.get('/filtered_body') { [200, {'Content-Type' => 'text/html'}, 'soylent green is people'] } + stubs.get('/filtered_headers') { [200, {'Content-Type' => 'text/html'}, 'headers response'] } + stubs.get('/filtered_params') { [200, {'Content-Type' => 'text/html'}, 'params response'] } + stubs.get('/filtered_url') { [200, {'Content-Type' => 'text/html'}, 'url response'] } end end end @@ -94,5 +102,30 @@ module Adapters app.get '/rubbles', nil, :accept => 'text/html' assert_match %([\"Barney\", \"Betty\", \"Bam Bam\"]\n), @io.string end + + def test_logs_filter_body + app = conn(@logger, :bodies => true) + app.get '/filtered_body', nil, :accept => 'text/html' + assert_match %(soylent green is), @io.string + assert_match %(tasty), @io.string + refute_match %(people), @io.string + end + + def test_logs_filter_headers + app = conn(@logger) + app.headers = {'api_key' => 'ABC123'} + app.get '/filtered_headers', nil, :accept => 'text/html' + assert_match %(api_key:), @io.string + assert_match %([API_KEY]), @io.string + refute_match %(ABC123), @io.string + end + + def test_logs_filter_url + app = conn(@logger) + app.get '/filtered_url?password=hunter2', nil, :accept => 'text/html' + assert_match %(password=[HIDDEN]), @io.string + refute_match %(hunter2), @io.string + end + end end