Repository: qpid-proton Updated Branches: refs/heads/master 060612666 -> 36b64f73f
PROTON-1532: ruby Container support for SASL, client and server. - add Container.connect options for client-side SASL settings - use on_connection_bound for server-side SASL settings based on incoming connection/transport For example code see proton-c/bindings/ruby/tests/test_container.rb Proper examples will be added later. Project: http://git-wip-us.apache.org/repos/asf/qpid-proton/repo Commit: http://git-wip-us.apache.org/repos/asf/qpid-proton/commit/36b64f73 Tree: http://git-wip-us.apache.org/repos/asf/qpid-proton/tree/36b64f73 Diff: http://git-wip-us.apache.org/repos/asf/qpid-proton/diff/36b64f73 Branch: refs/heads/master Commit: 36b64f73ffe7e9add93c32da622474d2ee29dce6 Parents: 0606126 Author: Alan Conway <acon...@redhat.com> Authored: Fri Aug 11 15:55:25 2017 -0400 Committer: Alan Conway <acon...@redhat.com> Committed: Tue Aug 22 15:41:48 2017 -0400 ---------------------------------------------------------------------- proton-c/CMakeLists.txt | 11 +- proton-c/bindings/ruby/lib/core/connection.rb | 15 +- proton-c/bindings/ruby/lib/core/message.rb | 4 +- proton-c/bindings/ruby/lib/core/sasl.rb | 104 ++++++---- proton-c/bindings/ruby/lib/core/transport.rb | 2 + proton-c/bindings/ruby/lib/core/url.rb | 16 +- .../ruby/lib/handler/endpoint_state_handler.rb | 2 +- proton-c/bindings/ruby/lib/reactor/connector.rb | 73 ++++--- proton-c/bindings/ruby/lib/reactor/container.rb | 77 +++---- proton-c/bindings/ruby/lib/reactor/reactor.rb | 1 - proton-c/bindings/ruby/lib/reactor/urls.rb | 7 +- proton-c/bindings/ruby/lib/util/condition.rb | 2 + proton-c/bindings/ruby/lib/util/swig_helper.rb | 2 +- proton-c/bindings/ruby/tests/test_container.rb | 200 +++++++++++++++++++ proton-c/bindings/ruby/tests/test_tools.rb | 193 ++++++++++++++++++ proton-c/src/sasl/cyrus_sasl.c | 6 +- 16 files changed, 603 insertions(+), 112 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/CMakeLists.txt ---------------------------------------------------------------------- diff --git a/proton-c/CMakeLists.txt b/proton-c/CMakeLists.txt index 054a054..2bfbb27 100644 --- a/proton-c/CMakeLists.txt +++ b/proton-c/CMakeLists.txt @@ -792,11 +792,12 @@ find_program(RUBY_EXE "ruby") if (RUBY_EXE AND BUILD_RUBY) set (rb_root "${pn_test_root}/ruby") set (rb_src "${CMAKE_CURRENT_SOURCE_DIR}/bindings/ruby") - set (rb_lib "${CMAKE_CURRENT_SOURCE_DIR}/bindings/ruby/lib") + set (rb_lib "${rb_src}/lib") + set (rb_tests "${rb_src}/tests") set (rb_bin "${CMAKE_CURRENT_BINARY_DIR}/bindings/ruby") set (rb_bld "$<TARGET_FILE_DIR:qpid-proton>") set (rb_path $ENV{PATH} ${rb_bin} ${rb_bld}) - set (rb_rubylib ${rb_root} ${rb_src} ${rb_bin} ${rb_bld} ${rb_lib}) + set (rb_rubylib ${rb_root} ${rb_src} ${rb_bin} ${rb_bld} ${rb_lib} ${rb_tests}) to_native_path("${rb_path}" rb_path) to_native_path("${rb_rubylib}" rb_rubylib) @@ -806,6 +807,12 @@ if (RUBY_EXE AND BUILD_RUBY) COMMAND ${env_py} -- "PATH=${rb_path}" "RUBYLIB=${rb_rubylib}" ${RUBY_EXE} example_test.rb -v) + # TODO aconway 2017-08-16: move test cmake code to ruby/tests directory + add_test(NAME ruby-container-test + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/proton-c/bindings/ruby + COMMAND ${env_py} -- "PATH=${rb_path}" "RUBYLIB=${rb_rubylib}" + ${RUBY_EXE} ${rb_tests}/test_container.rb -v) + # ruby unit tests: tests/ruby/proton-test # only enable the tests if the Ruby gem dependencies were found if (DEFAULT_RUBY_TESTING) http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/core/connection.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/core/connection.rb b/proton-c/bindings/ruby/lib/core/connection.rb index c1bcaf3..949ff2a 100644 --- a/proton-c/bindings/ruby/lib/core/connection.rb +++ b/proton-c/bindings/ruby/lib/core/connection.rb @@ -19,7 +19,7 @@ module Qpid::Proton - # A Connection option has at most one Qpid::Proton::Transport instance. + # A Connection has at most one Qpid::Proton::Transport instance. # class Connection < Endpoint @@ -35,6 +35,19 @@ module Qpid::Proton # proton_accessor :hostname + # @!attribute user + # The user name for authentication. + # + # A client sets authentication data with the :user and :password options + # to {Container#connect}. On a server this returns the authenticated name + # from the client. It makes no sense to set this on the server side. + # + # @return [String] the user name + proton_accessor :user + + # @private + proton_writer :password + # @private proton_reader :attachments http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/core/message.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/core/message.rb b/proton-c/bindings/ruby/lib/core/message.rb index 26a3ae2..81bbadb 100644 --- a/proton-c/bindings/ruby/lib/core/message.rb +++ b/proton-c/bindings/ruby/lib/core/message.rb @@ -129,13 +129,13 @@ module Qpid::Proton end # Creates a new +Message+ instance. - def initialize + def initialize(body = nil) @impl = Cproton.pn_message ObjectSpace.define_finalizer(self, self.class.finalize!(@impl)) @properties = {} @instructions = {} @annotations = {} - @body = nil + self.body = body unless body.nil? end def to_s http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/core/sasl.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/core/sasl.rb b/proton-c/bindings/ruby/lib/core/sasl.rb index be57044..965c889 100644 --- a/proton-c/bindings/ruby/lib/core/sasl.rb +++ b/proton-c/bindings/ruby/lib/core/sasl.rb @@ -28,19 +28,7 @@ module Qpid::Proton # The peer acting as the SASL server must provide authentication against the # received credentials. # - # @example - # # SCENARIO: the remote endpoint has not initialized their connection - # # then the local endpoint, acting as a SASL server, decides - # # to allow an anonymous connection. - # # - # # The SASL layer locally assumes the role of server and then - # # enables anonymous authentication for the remote endpoint. - # # - # sasl = @transport.sasl - # sasl.server - # sasl.mechanisms("ANONYMOUS") - # sasl.done(Qpid::Proton::SASL::OK) - # + # @note Do not instantiate directly, use {Transport#sasl} to create a SASL object. class SASL # Negotation has not completed. @@ -50,45 +38,89 @@ module Qpid::Proton # Authentication failed due to bad credentials. AUTH = Cproton::PN_SASL_AUTH - # Constructs a new instance for the given transport. - # - # @param transport [Transport] The transport. - # - # @private A SASL should be fetched only from its Transport - # + private + + include Util::SwigHelper + PROTON_METHOD_PREFIX = "pn_sasl" + + public + + # @private + # @note Do not instantiate directly, use {Transport#sasl} to create a SASL object. def initialize(transport) @impl = Cproton.pn_sasl(transport.impl) end - # Sets the acceptable SASL mechanisms. + # @!attribute allow_insecure_mechs + # @return [Bool] true if clear text authentication is allowed on insecure connections. + proton_accessor :allow_insecure_mechs + + # @!attribute user [r] + # @return [String] the authenticated user name + proton_reader :user + + # Set the mechanisms allowed for SASL negotation + # @param mechanisms [String] space-delimited list of allowed mechanisms + def allowed_mechs=(mechanisms) + Cproton.pn_sasl_allowed_mechs(@impl, mechanisms) + end + + # @deprecated use {#allowed_mechs=} + def mechanisms(m) + self.allowed_mechs = m + end + + # True if extended SASL negotiation is supported # - # @param mechanisms [String] The space-delimited set of mechanisms. + # All implementations of Proton support ANONYMOUS and EXTERNAL on both + # client and server sides and PLAIN on the client side. # - # @example Use anonymous SASL authentication. - # @sasl.mechanisms("GSSAPI CRAM-MD5 PLAIN") + # Extended SASL implememtations use an external library (Cyrus SASL) + # to support other mechanisms. # - def mechanisms(mechanisms) - Cproton.pn_sasl_mechanisms(@impl, mechanisms) + # @return [Bool] true if extended SASL negotiation is supported + def self.extended?() + Cproton.pn_sasl_extended() end - # Returns the outcome of the SASL negotiation. + # Set the sasl configuration path + # + # This is used to tell SASL where to look for the configuration file. + # In the current implementation it can be a colon separated list of directories. + # + # The environment variable PN_SASL_CONFIG_PATH can also be used to set this path, + # but if both methods are used then this pn_sasl_config_path() will take precedence. + # + # If not set the underlying implementation default will be used. # - # @return [Integer] The outcome. + # @param path the configuration path # - def outcome - outcome = Cprotn.pn_sasl_outcome(@impl) - return nil if outcome == NONE - outcome + def self.config_path=(path) + Cproton.pn_sasl_config_path(nil, path) + path end - # Set the condition of the SASL negotiation. + # @deprecated use {config_path=} + def self.config_path(path) + self.config_path = path + end + + # Set the configuration file name, without extension + # + # The name with an a ".conf" extension will be searched for in the + # configuration path. If not set, it defaults to "proton-server" or + # "proton-client" for a server (incoming) or client (outgoing) connection + # respectively. # - # @param outcome [Integer] The outcome. + # @param name the configuration file name without extension # - def done(outcome) - Cproton.pn_sasl_done(@impl, outcome) + def self.config_name=(name) + Cproton.pn_sasl_config_name(nil, name) end + # @deprecated use {config_name=} + def self.config_name(name) + self.config_name = name + end end - end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/core/transport.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/core/transport.rb b/proton-c/bindings/ruby/lib/core/transport.rb index c3cacc7..04697a3 100644 --- a/proton-c/bindings/ruby/lib/core/transport.rb +++ b/proton-c/bindings/ruby/lib/core/transport.rb @@ -386,6 +386,8 @@ module Qpid::Proton Cproton.pn_transport_tick(@impl, now) end + # Create, or return existing, SSL object for the transport. + # @return [SASL] the SASL object def sasl SASL.new(self) end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/core/url.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/core/url.rb b/proton-c/bindings/ruby/lib/core/url.rb index 1fa1222..39b6465 100644 --- a/proton-c/bindings/ruby/lib/core/url.rb +++ b/proton-c/bindings/ruby/lib/core/url.rb @@ -28,11 +28,13 @@ module Qpid::Proton attr_reader :port attr_reader :path + # Parse a string, return a new URL + # @param url [#to_s] the URL string def initialize(url = nil, options = {}) options[:defaults] = true if url - @url = Cproton.pn_url_parse(url) + @url = Cproton.pn_url_parse(url.to_s) if @url.nil? raise ::ArgumentError.new("invalid url: #{url}") end @@ -64,6 +66,11 @@ module Qpid::Proton "#{@scheme}://#{@username.nil? ? '' : @username}#{@password.nil? ? '' : '@' + @password + ':'}#{@host}:#{@port}/#{@path}" end + # Return self + def to_url() + self + end + private def defaults @@ -71,7 +78,12 @@ module Qpid::Proton @host = @host || "0.0.0.0" @port = @port || 5672 end - end +end +class String + # Convert this string to a URL + def to_url() + return URL.new(self) + end end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/handler/endpoint_state_handler.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/handler/endpoint_state_handler.rb b/proton-c/bindings/ruby/lib/handler/endpoint_state_handler.rb index 727a20b..11e970a 100644 --- a/proton-c/bindings/ruby/lib/handler/endpoint_state_handler.rb +++ b/proton-c/bindings/ruby/lib/handler/endpoint_state_handler.rb @@ -119,7 +119,7 @@ module Qpid::Proton::Handler end def on_connection_opened(event) - Qpid::Proton::Event.dispatch(@delegate, :on_session_opened, event) if !@delegate.nil? + Qpid::Proton::Event.dispatch(@delegate, :on_connection_opened, event) if !@delegate.nil? end def on_session_opened(event) http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/reactor/connector.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/reactor/connector.rb b/proton-c/bindings/ruby/lib/reactor/connector.rb index a6523db..0971141 100644 --- a/proton-c/bindings/ruby/lib/reactor/connector.rb +++ b/proton-c/bindings/ruby/lib/reactor/connector.rb @@ -21,16 +21,24 @@ module Qpid::Proton::Reactor class Connector < Qpid::Proton::BaseHandler - attr_accessor :address - attr_accessor :reconnect - attr_accessor :ssl_domain + def initialize(connection, url, opts) + @connection, @opts = connection, opts + @urls = URLs.new(url) if url + opts.each do |k,v| + case k + when :url, :urls, :address + @urls = URLs.new(v) unless @urls + when :reconnect + @reconnect = v + end + end + raise ::ArgumentError.new("no url for connect") unless @urls - def initialize(connection) - @connection = connection - @address = nil - @heartbeat = nil - @reconnect = nil - @ssl_domain = nil + # TODO aconway 2017-08-17: review reconnect configuration and defaults + @reconnect = Backoff.new() unless @reconnect + @ssl_domain = SessionPerConnection.new # TODO seems this should be configurable + @connection.overrides = self + @connection.open end def on_connection_local_open(event) @@ -38,10 +46,7 @@ module Qpid::Proton::Reactor end def on_connection_remote_open(event) - if !@reconnect.nil? - @reconnect.reset - @transport = nil - end + @reconnect.reset if @reconnect end def on_transport_tail_closed(event) @@ -73,26 +78,38 @@ module Qpid::Proton::Reactor end def connect(connection) - url = @address.next + url = @urls.next + transport = Qpid::Proton::Transport.new + @opts.each do |k,v| + case k + when :user + connection.user = v + when :password + connection.password = v + when :heartbeat + transport.idle_timeout = v.to_i + when :idle_timeout + transport.idle_timeout = v.(v*1000).to_i + when :sasl_enabled + transport.sasl if v + when :sasl_allow_insecure_mechs + transport.sasl.allow_insecure_mechs = v + when :sasl_allowed_mechs, :sasl_mechanisms + transport.sasl.allowed_mechs = v + end + end + + # TODO aconway 2017-08-11: hostname setting is incorrect, reactor only connection.hostname = "#{url.host}:#{url.port}" + connection.user = url.username if url.username && !url.username.empty? + connection.password = url.password if url.password && !url.password.empty? - transport = Qpid::Proton::Transport.new transport.bind(connection) - if !@heartbeat.nil? - transport.idle_timeout = @heartbeat - elsif (url.scheme == "amqps") && !@ssl_domain.nil? + + if (url.scheme == "amqps") && @ssl_domain @ssl = Qpid::Proton::SSL.new(transport, @ssl_domain) - @ss.peer_hostname = url.host - elsif !url.username.nil? - sasl = transport.sasl - if url.username == "anonymous" - sasl.mechanisms("ANONYMOUS") - else - sasl.plain(url.username, url.password) - end + @ssl.peer_hostname = url.host end end - end - end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/reactor/container.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/reactor/container.rb b/proton-c/bindings/ruby/lib/reactor/container.rb index 2a7a030..aa1b303 100644 --- a/proton-c/bindings/ruby/lib/reactor/container.rb +++ b/proton-c/bindings/ruby/lib/reactor/container.rb @@ -19,7 +19,7 @@ module Qpid::Proton::Reactor - # @private + private class InternalTransactionHandler < Qpid::Proton::Handler::OutgoingMessageHandler def initialize @@ -35,7 +35,7 @@ module Qpid::Proton::Reactor end - + public # A representation of the AMQP concept of a container which, loosely # speaking, is something that establishes links to or from another # container on which messages are transferred. @@ -74,42 +74,49 @@ module Qpid::Proton::Reactor end end - # Initiates the establishment of an AMQP connection. + # TODO aconway 2017-08-17: fill out options + + # Connects to a remote AMQP endpoint and sends an AMQP "open" frame. # - # @param options [Hash] A hash of named arguments. + # @param url [#to_url] Connect to URL host:port. + # If URL has user:password use them for authentication. # - def connect(options = {}) - conn = self.connection(options[:handler]) - conn.container = self.container_id || generate_uuid - connector = Connector.new(conn) - conn.overrides = connector - if !options[:url].nil? - connector.address = URLs.new([options[:url]]) - elsif !options[:urls].nil? - connector.address = URLs.new(options[:urls]) - elsif !options[:address].nil? - connector.address = URLs.new([Qpid::Proton::URL.new(options[:address])]) - else - raise ::ArgumentError.new("either :url or :urls or :address required") - end - - connector.heartbeat = options[:heartbeat] if !options[:heartbeat].nil? - if !options[:reconnect].nil? - connector.reconnect = options[:reconnect] - else - connector.reconnect = Backoff.new() + # @option opts [String] :user user name for authentication if not given by URL + # @option opts [String] :password password for authentication if not given by URL + # + # @option opts [Numeric] :idle_timeout seconds before closing an idle connection + # + # @option opts [Boolean] :sasl_enabled Enable or disable SASL. + # + # @option opts [Boolean] :sasl_allow_insecure_mechs Allow mechanisms that disclose clear text + # passwords, even over an insecure connection. By default, such mechanisms are only allowed + # when SSL is enabled. + # + # @option opts [String] :sasl_allowed_mechs the allowed SASL mechanisms for use on the connection. + # + # @param url [Hash] *deprecated* if url is a Hash and opts is unspecified, treat it as opts. + # @option opts [#to_url] :url *deprecated* use the url parameter + # @option opts [Enumerable<#to_url>] :urls *deprecated* use the url parameter + # @option opts [#to_url] :address *deprecated* use the url parameter + # @option opts [#to_url] :heartbeat *deprecated* alias for :idle_timeout, but in milliseconds + # @return [Connection] the new connection + # + def connect(url, opts = {}) + # Backwards compatible with old connect(options) + if url.is_a? Hash and opts.empty? + opts = url + url = nil end - - connector.ssl_domain = SessionPerConnection.new # TODO seems this should be configurable - - conn.open - + conn = self.connection(opts[:handler]) + conn.container = self.container_id || generate_uuid + connector = Connector.new(conn, url, opts) return conn end + private def _session(context) if context.is_a?(Qpid::Proton::URL) - return self._session(self.connect(:url => context)) + return _session(self.connect(:url => context)) elsif context.is_a?(Qpid::Proton::Session) return context elsif context.is_a?(Qpid::Proton::Connection) @@ -123,6 +130,7 @@ module Qpid::Proton::Reactor end end + public # Initiates the establishment of a link over which messages can be sent. # # @param context [String, URL] The context. @@ -146,7 +154,7 @@ module Qpid::Proton::Reactor target = context.path end - session = self._session(context) + session = _session(context) sender = session.sender(opts[:name] || id(session.connection.container, @@ -155,7 +163,7 @@ module Qpid::Proton::Reactor sender.target.address = target if target sender.handler = opts[:handler] if !opts[:handler].nil? sender.tag_generator = opts[:tag_generator] if !opts[:tag_gnenerator].nil? - self._apply_link_options(opts[:options], sender) + _apply_link_options(opts[:options], sender) sender.open return sender end @@ -192,7 +200,7 @@ module Qpid::Proton::Reactor source = context.path end - session = self._session(context) + session = _session(context) receiver = session.receiver(opts[:name] || id(session.connection.container, @@ -201,7 +209,7 @@ module Qpid::Proton::Reactor receiver.source.dynamic = true if opts.has_key?(:dynamic) && opts[:dynamic] receiver.target.address = opts[:target] if !opts[:target].nil? receiver.handler = opts[:handler] if !opts[:handler].nil? - self._apply_link_options(opts[:options], receiver) + _apply_link_options(opts[:options], receiver) receiver.open return receiver end @@ -236,6 +244,7 @@ module Qpid::Proton::Reactor return acceptor end + private def do_work(timeout = nil) self.timeout = timeout unless timeout.nil? self.process http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/reactor/reactor.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/reactor/reactor.rb b/proton-c/bindings/ruby/lib/reactor/reactor.rb index a84a716..f612876 100644 --- a/proton-c/bindings/ruby/lib/reactor/reactor.rb +++ b/proton-c/bindings/ruby/lib/reactor/reactor.rb @@ -115,7 +115,6 @@ module Qpid::Proton::Reactor end def run(&block) - self.timeout = 3.14159265359 self.start while self.process do if block_given? http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/reactor/urls.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/reactor/urls.rb b/proton-c/bindings/ruby/lib/reactor/urls.rb index 8cdb16c..44fd956 100644 --- a/proton-c/bindings/ruby/lib/reactor/urls.rb +++ b/proton-c/bindings/ruby/lib/reactor/urls.rb @@ -22,7 +22,12 @@ module Qpid::Proton::Reactor class URLs def initialize(values) - @values = [values].flatten + @values = values + if @values.is_a? Enumerable + @values = @values.map { |u| u.to_url } + else + @values = [values.to_url] + end @iter = @values.each end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/util/condition.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/util/condition.rb b/proton-c/bindings/ruby/lib/util/condition.rb index b8fd94b..ad49595 100644 --- a/proton-c/bindings/ruby/lib/util/condition.rb +++ b/proton-c/bindings/ruby/lib/util/condition.rb @@ -21,6 +21,8 @@ module Qpid::Proton::Util class Condition + attr_reader :name, :description, :info + def initialize(name, description = nil, info = nil) @name = name @description = description http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/lib/util/swig_helper.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/lib/util/swig_helper.rb b/proton-c/bindings/ruby/lib/util/swig_helper.rb index d60e9e4..5567235 100644 --- a/proton-c/bindings/ruby/lib/util/swig_helper.rb +++ b/proton-c/bindings/ruby/lib/util/swig_helper.rb @@ -86,7 +86,7 @@ module Qpid::Proton::Util proton_method = "#{self::PROTON_METHOD_PREFIX}_#{name}" # drop the trailing '?' if this is a property method proton_method = proton_method[0..-2] if proton_method.end_with? "?" - create_wrapper_method(name, proton_method) + create_wrapper_method(name, proton_method, options[:arg]) end def proton_writer(name, options = {}) http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/tests/test_container.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/tests/test_container.rb b/proton-c/bindings/ruby/tests/test_container.rb new file mode 100644 index 0000000..7ed81b2 --- /dev/null +++ b/proton-c/bindings/ruby/tests/test_container.rb @@ -0,0 +1,200 @@ +#-- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +#++ + +require 'test_tools' + +Message = Qpid::Proton::Message +SASL = Qpid::Proton::SASL +URL = Qpid::Proton::URL + +class ContainerTest < Minitest::Test + + # Send n messages + class SendMessageClient < TestHandler + attr_reader :accepted + + def initialize(url, link_name, body) + super() + @url, @link_name, @message = url, link_name, Message.new(body) + end + + def on_start(event) + event.container.create_sender(@url, {:name => @link_name}) + end + + def on_sendable(event) + if event.sender.credit > 0 + event.sender.send(@message) + end + end + + def on_accepted(event) + @accepted = event + event.connection.close + end + end + + def test_simple() + TestServer.new.run do |s| + lname = "test-link" + body = "hello" + c = SendMessageClient.new(s.addr, lname, body).run + assert_instance_of(Qpid::Proton::Event::Event, c.accepted) + assert_equal(lname, s.links.pop(true).name) + assert_equal(body, s.messages.pop(true).body) + end + end + +end + +class ContainerSASLTest < Minitest::Test + + # Connect to URL using mechanisms and insecure to configure the transport + class SASLClient < TestHandler + + def initialize(url, opts={}) + super() + @url, @opts = url, opts + end + + def on_start(event) + event.container.connect(@url, @opts) + end + + def on_connection_opened(event) + super + event.container.stop + end + end + + # Server with SASL settings + class SASLServer < TestServer + def initialize(mechanisms=nil, insecure=nil, realm=nil) + super() + @mechanisms, @insecure, @realm = mechanisms, insecure, realm + end + + def on_connection_bound(event) + sasl = event.transport.sasl + sasl.allow_insecure_mechs = @insecure unless @insecure.nil? + sasl.allowed_mechs = @mechanisms unless @mechanisms.nil? + # TODO aconway 2017-08-16: need `sasl.realm(@realm)` here for non-default realms. + # That reqiures pn_sasl_set_realm() at the C layer - the realm should + # be passed to cyrus_sasl_init_server() + end + end + + # Generate SASL server configuration files and database, initialize proton SASL + class SASLConfig + attr_reader :conf_dir, :conf_file, :conf_name, :database + + def initialize() + if SASL.extended? # Configure cyrus SASL + @conf_dir = File.expand_path('sasl_conf') + @conf_name = "proton-server" + @database = File.join(@conf_dir, "proton.sasldb") + @conf_file = File.join(conf_dir,"#{@conf_name}.conf") + Dir::mkdir(@conf_dir) unless File.directory?(@conf_dir) + # Same user name in different realms + make_user("user", "password", "proton") # proton realm + make_user("user", "default_password") # Default realm + File.open(@conf_file, 'w') do |f| + f.write(" +sasldb_path: #{database} +mech_list: EXTERNAL DIGEST-MD5 SCRAM-SHA-1 CRAM-MD5 PLAIN ANONYMOUS +") + end + # Tell proton library to use the new configuration + SASL.config_path(conf_dir) + SASL.config_name(conf_name) + end + end + + private + + SASLPASSWD = (ENV['SASLPASSWD'] or 'saslpasswd2') + + def make_user(user, password, realm=nil) + realm_opt = (realm ? "-u #{realm}" : "") + cmd = "echo '#{password}' | #{SASLPASSWD} -c -p -f #{database} #{realm_opt} #{user}" + system(cmd) or raise RuntimeError.new("saslpasswd2 failed: #{makepw_cmd}") + end + DEFAULT = SASLConfig.new + end + + def test_sasl_anonymous() + SASLServer.new("ANONYMOUS").run do |s| + c = SASLClient.new(s.addr, {:sasl_allowed_mechs => "ANONYMOUS"}).run + refute_empty(c.connections) + refute_empty(s.connections) + assert_nil(s.connections.pop(true).user) + end + end + + def test_sasl_plain_url() + # Use default realm with URL, should authenticate with "default_password" + SASLServer.new("PLAIN", true).run do |s| + c = SASLClient.new("amqp://user:default_password@#{s.addr}", + {:sasl_allowed_mechs => "PLAIN", :sasl_allow_insecure_mechs => true}).run + refute_empty(c.connections) + refute_empty(s.connections) + sc = s.connections.pop(true) + assert_equal("user", sc.transport.sasl.user) + end + end + + def test_sasl_plain_options() + # Use default realm with connection options, should authenticate with "default_password" + SASLServer.new("PLAIN", true).run do |s| + c = SASLClient.new(s.addr, + {:user => "user", :password => "default_password", + :sasl_allowed_mechs => "PLAIN", :sasl_allow_insecure_mechs => true}).run + refute_empty(c.connections) + refute_empty(s.connections) + sc = s.connections.pop(true) + assert_equal("user", sc.transport.sasl.user) + end + end + + # Test disabled, see on_connection_bound - missing realm support in proton C. + def TODO_test_sasl_plain_realm() + # Use the non-default proton realm on the server, should authenticate with "password" + SASLServer.new("PLAIN", true, "proton").run do |s| + c = SASLClient.new("amqp://user:password@#{s.addr}", + {:sasl_allowed_mechs => "PLAIN", :sasl_allow_insecure_mechs => true}).run + refute_empty(c.connections) + refute_empty(s.connections) + sc = s.connections.pop(true) + assert_equal("user", sc.transport.sasl.user) + end + end + + # Ensure we don't allow PLAIN if allow_insecure_mechs = true is not explicitly set + def test_disallow_insecure() + # Don't set allow_insecure_mechs, but try to use PLAIN + SASLServer.new("PLAIN", nil).run(true) do |s| + begin + SASLClient.new("amqp://user:password@#{s.addr}", + {:sasl_allowed_mechs => "PLAIN", :sasl_allow_insecure_mechs => true}).run + rescue TestError => e + assert_match(/PN_TRANSPORT_ERROR.*unauthorized-access/, e.to_s) + end + end + end +end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/bindings/ruby/tests/test_tools.rb ---------------------------------------------------------------------- diff --git a/proton-c/bindings/ruby/tests/test_tools.rb b/proton-c/bindings/ruby/tests/test_tools.rb new file mode 100644 index 0000000..a48a508 --- /dev/null +++ b/proton-c/bindings/ruby/tests/test_tools.rb @@ -0,0 +1,193 @@ +#-- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +#++ + +# Tools for tests + +require 'minitest/autorun' +require 'qpid_proton' +require 'thread' +require 'socket' + +Container = Qpid::Proton::Reactor::Container +MessagingHandler = Qpid::Proton::Handler::MessagingHandler + +# Bind an unused local port using bind(0) and SO_REUSEADDR and hold it till close() +# Provides #host, #port and #addr ("host:port") as strings +class TestPort + attr_reader :host, :port, :addr + + # With block, execute block passing self then close + # Note host must be the local host, but you can pass '::1' instead for ipv6 + def initialize(host='127.0.0.1') + @sock = Socket.new(:INET, :STREAM) + @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) + @sock.bind(Socket.sockaddr_in(0, host)) + @host, @port = @sock.connect_address.ip_unpack + @addr = "#{@host}:#{@port}" + if block_given? + begin + yield self + ensure + close + end + end + end + + def close + @sock.close() + end +end + +class TestError < Exception; end + +# Handler that creates its own container to run itself, and records some common +# events that are checked by tests +class TestHandler < MessagingHandler + + # Record errors and successfully opened endpoints + attr_reader :errors, :connections, :sessions, :links, :messages + + # Pass optional extra handlers and options to the Container + def initialize(handlers=[], options={}) + super() + # Use Queue so the values can be extracted in a thread-safe way during or after a test. + @errors, @connections, @sessions, @links, @messages = (1..5).collect { Queue.new } + @container = Container.new([self]+handlers, options) + end + + # Run the handlers container, return self. + # Raise an exception for server errors unless no_raise is true. + def run(no_raise=false) + @container.run + raise_errors unless no_raise + self + end + + # If the handler has errors, raise a TestError with all the error text + def raise_errors() + return if @errors.empty? + text = "" + while @errors.size > 0 + text << @errors.pop + "\n" + end + raise TestError.new("TestServer has errors:\n #{text}") + end + + # TODO aconway 2017-08-15: implement in MessagingHandler + def on_error(event, endpoint) + @errors.push "#{event.type}: #{endpoint.condition.name}: #{endpoint.condition.description}" + raise_errors + end + + def on_transport_error(event) + on_error(event, event.transport) + end + + def on_connection_error(event) + on_error(event, event.condition) + end + + def on_session_error(event) + on_error(event, event.session) + end + + def on_link_error(event) + on_error(event, event.link) + end + + def on_opened(queue, endpoint) + queue.push(endpoint) + endpoint.open + end + + def on_connection_opened(event) + on_opened(@connections, event.connection) + end + + def on_session_opened(event) + on_opened(@sessions, event.session) + end + + def on_link_opened(event) + on_opened(@links, event.link) + end + + def on_message(event) + @messages.push(event.message) + end +end + +# A TestHandler that runs itself in a thread and listens on a TestPort +class TestServer < TestHandler + attr_reader :host, :port, :addr + + # Pass optional handlers, options to the container + def initialize(handlers=[], options={}) + super + @tp = TestPort.new + @host, @port, @addr = @tp.host, @tp.port, @tp.addr + @listening = false + @ready = Queue.new + end + + # Start server thread + def start(no_raise=false) + @thread = Thread.new do + begin + @container.listen(addr) + @container.run + rescue TestError + ready.push :error + rescue => e + msg = "TestServer run raised: #{e.message}\n#{e.backtrace.join("\n")}" + @errors << msg + @ready.push(:error) + # TODO aconway 2017-08-22: container.stop - doesn't stop the thread. + end + end + raise_errors unless @ready.pop == :listening or no_raise + end + + # Stop server thread + def stop(no_raise=false) + @container.stop + if not @errors.empty? + @thread.kill + else + @thread.join + end + @tp.close + raise_errors unless no_raise + end + + # start(), execute block with self, stop() + def run(no_raise=false) + begin + start(no_raise) + yield self + ensure + stop(no_raise) + end + end + + def on_start(event) + @ready.push :listening + @listening = true + end +end http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/36b64f73/proton-c/src/sasl/cyrus_sasl.c ---------------------------------------------------------------------- diff --git a/proton-c/src/sasl/cyrus_sasl.c b/proton-c/src/sasl/cyrus_sasl.c index 88bdd7a..ab6eba6 100644 --- a/proton-c/src/sasl/cyrus_sasl.c +++ b/proton-c/src/sasl/cyrus_sasl.c @@ -180,9 +180,9 @@ void pn_sasl_config_name(pn_sasl_t *sasl0, const char *name) void pn_sasl_config_path(pn_sasl_t *sasl0, const char *dir) { - if (!pni_cyrus_config_dir) { - pni_cyrus_config_dir = strdup(dir); - } + if (!pni_cyrus_config_dir) { + pni_cyrus_config_dir = strdup(dir); + } } __attribute__((destructor)) --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org