diff --git a/docs/static/configuration.asciidoc b/docs/static/configuration.asciidoc index d78347857..90e51154b 100644 --- a/docs/static/configuration.asciidoc +++ b/docs/static/configuration.asciidoc @@ -203,6 +203,21 @@ Example: my_password => "password" ---------------------------------- +[[uri]] +[float] +==== URI + +A URI can be anything from a full URL like 'http://elastic.co/' to a simple identifier +like 'foobar'. If the URI contains a password such as 'http://user:pass@example.net' the password +portion of the URI will not be logged or printed. + +Example: +[source,js] +---------------------------------- + my_uri => "http://foo:bar@example.net" +---------------------------------- + + [[path]] [float] ==== Path diff --git a/logstash-core/lib/logstash/config/mixin.rb b/logstash-core/lib/logstash/config/mixin.rb index d5a1637f3..6929b6eed 100644 --- a/logstash-core/lib/logstash/config/mixin.rb +++ b/logstash-core/lib/logstash/config/mixin.rb @@ -4,6 +4,7 @@ require "logstash/config/registry" require "logstash/plugins/registry" require "logstash/logging" require "logstash/util/password" +require "logstash/util/safe_uri" require "logstash/version" require "logstash/environment" require "logstash/util/plugin_version" @@ -513,6 +514,12 @@ module LogStash::Config::Mixin end result = value.first.is_a?(::LogStash::Util::Password) ? value.first : ::LogStash::Util::Password.new(value.first) + when :uri + if value.size > 1 + return false, "Expected uri (one value), got #{value.size} values?" + end + + result = value.first.is_a?(::LogStash::Util::SafeURI) ? value.first : ::LogStash::Util::SafeURI.new(value.first) when :path if value.size > 1 # Only 1 value wanted return false, "Expected path (one value), got #{value.size} values?" @@ -551,6 +558,10 @@ module LogStash::Config::Mixin if @config[key][:validate] == :password && !value.is_a?(::LogStash::Util::Password) params[key] = ::LogStash::Util::Password.new(value) end + + if @config[key][:validate] == :uri && !value.is_a?(::LogStash::Util::SafeURI) + params[key] = ::LogStash::Util::SafeURI.new(value) + end end end diff --git a/logstash-core/lib/logstash/util/safe_uri.rb b/logstash-core/lib/logstash/util/safe_uri.rb new file mode 100644 index 000000000..9d24386e6 --- /dev/null +++ b/logstash-core/lib/logstash/util/safe_uri.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 +require "logstash/namespace" +require "logstash/util" + +# This class exists to quietly wrap a password string so that, when printed or +# logged, you don't accidentally print the password itself. +class LogStash::Util::SafeURI + PASS_PLACEHOLDER = "xxxxxx".freeze + + extend Forwardable + + def_delegators :@uri, :coerce, :query=, :route_from, :port=, :default_port, :select, :normalize!, :absolute?, :registry=, :path, :password, :hostname, :merge, :normalize, :host, :component_ary, :userinfo=, :query, :set_opaque, :+, :merge!, :-, :password=, :parser, :port, :set_host, :set_path, :opaque=, :scheme, :fragment=, :set_query, :set_fragment, :userinfo, :hostname=, :set_port, :path=, :registry, :opaque, :route_to, :set_password, :hierarchical?, :set_user, :set_registry, :set_userinfo, :fragment, :component, :user=, :set_scheme, :absolute, :host=, :relative?, :scheme=, :user + + attr_reader :uri + + public + def initialize(arg) + @uri = case arg + when String + URI.parse(arg) + when URI + arg + else + raise ArgumentError, "Expected a string or URI, got a #{arg.class} creating a URL" + end + end + + def to_s + sanitized.to_s + end + + def inspect + sanitized.to_s + end + + def sanitized + return uri unless uri.password # nothing to sanitize here! + + safe = uri.clone + safe.password = PASS_PLACEHOLDER + safe + end +end + diff --git a/logstash-core/spec/logstash/config/mixin_spec.rb b/logstash-core/spec/logstash/config/mixin_spec.rb index ca20d3649..5c18b6e88 100644 --- a/logstash-core/spec/logstash/config/mixin_spec.rb +++ b/logstash-core/spec/logstash/config/mixin_spec.rb @@ -102,6 +102,76 @@ describe LogStash::Config::Mixin do end end + context "when validating :uri" do + let(:klass) do + Class.new(LogStash::Filters::Base) do + config_name "fakeuri" + config :uri, :validate => :uri + end + end + + shared_examples("safe URI") do + subject { klass.new("uri" => uri_str) } + + it "should be a SafeURI object" do + expect(subject.uri).to(be_a(LogStash::Util::SafeURI)) + end + + it "should make password values hidden with #to_s" do + expect(subject.uri.to_s).to eql(uri_hidden) + end + + it "should make password values hidden with #inspect" do + expect(subject.uri.inspect).to eql(uri_hidden) + end + + it "should correctly copy URI types" do + clone = subject.class.new(subject.params) + expect(clone.uri.to_s).to eql(uri_hidden) + end + + it "should make the real URI object availale under #uri" do + expect(subject.uri.uri).to be_a(::URI) + end + + it "should obfuscate original_params" do + expect(subject.original_params['uri']).to(be_a(LogStash::Util::SafeURI)) + end + + context "attributes" do + [:scheme, :user, :password, :hostname, :path].each do |attr| + it "should make #{attr} available" do + expect(subject.uri.send(attr)).to eql(self.send(attr)) + end + end + end + end + + context "with a username / password" do + let(:scheme) { "myscheme" } + let(:user) { "myuser" } + let(:password) { "fancypants" } + let(:hostname) { "myhostname" } + let(:path) { "/my/path" } + let(:uri_str) { "#{scheme}://#{user}:#{password}@#{hostname}#{path}" } + let(:uri_hidden) { "#{scheme}://#{user}:#{LogStash::Util::SafeURI::PASS_PLACEHOLDER}@#{hostname}#{path}" } + + include_examples("safe URI") + end + + context "without a username / password" do + let(:scheme) { "myscheme" } + let(:user) { nil } + let(:password) { nil } + let(:hostname) { "myhostname" } + let(:path) { "/my/path" } + let(:uri_str) { "#{scheme}://#{hostname}#{path}" } + let(:uri_hidden) { "#{scheme}://#{hostname}#{path}" } + + include_examples("safe URI") + end + end + describe "obsolete settings" do let(:plugin_class) do Class.new(LogStash::Inputs::Base) do