Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rubygem-ruby-dbus for 
openSUSE:Factory checked in at 2023-03-24 15:16:04
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rubygem-ruby-dbus (Old)
 and      /work/SRC/openSUSE:Factory/.rubygem-ruby-dbus.new.31432 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rubygem-ruby-dbus"

Fri Mar 24 15:16:04 2023 rev:33 rq:1073585 version:0.20.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/rubygem-ruby-dbus/rubygem-ruby-dbus.changes      
2023-01-20 17:37:30.692133387 +0100
+++ 
/work/SRC/openSUSE:Factory/.rubygem-ruby-dbus.new.31432/rubygem-ruby-dbus.changes
   2023-03-24 15:16:07.661531707 +0100
@@ -1,0 +2,9 @@
+Tue Mar 21 15:39:33 UTC 2023 - Martin Vidner <mvid...@suse.com>
+
+- 0.20.0
+ Features:
+ * For EXTERNAL authentication, try also without the user id, to work with
+   containers (gh#mvidner/ruby-dbus#126).
+ * Thread safety, as long as the non-main threads only send signals.
+
+-------------------------------------------------------------------

Old:
----
  ruby-dbus-0.19.0.gem

New:
----
  ruby-dbus-0.20.0.gem

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rubygem-ruby-dbus.spec ++++++
--- /var/tmp/diff_new_pack.Q6OUpa/_old  2023-03-24 15:16:08.173534441 +0100
+++ /var/tmp/diff_new_pack.Q6OUpa/_new  2023-03-24 15:16:08.173534441 +0100
@@ -24,7 +24,7 @@
 #
 
 Name:           rubygem-ruby-dbus
-Version:        0.19.0
+Version:        0.20.0
 Release:        0
 %define mod_name ruby-dbus
 %define mod_full_name %{mod_name}-%{version}

++++++ ruby-dbus-0.19.0.gem -> ruby-dbus-0.20.0.gem ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/NEWS.md new/NEWS.md
--- old/NEWS.md 2023-01-18 14:34:24.000000000 +0100
+++ new/NEWS.md 2023-03-21 16:51:02.000000000 +0100
@@ -2,6 +2,15 @@
 
 ## Unreleased
 
+## Ruby D-Bus 0.20.0 - 2023-03-21
+
+Features:
+ * For EXTERNAL authentication, try also without the user id, to work with
+   containers ([#126][]).
+ * Thread safety, as long as the non-main threads only send signals.
+
+[#126]: https://github.com/mvidner/ruby-dbus/issues/126
+
 ## Ruby D-Bus 0.19.0 - 2023-01-18
 
 API:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/VERSION new/VERSION
--- old/VERSION 2023-01-18 14:34:24.000000000 +0100
+++ new/VERSION 2023-03-21 16:51:02.000000000 +0100
@@ -1 +1 @@
-0.19.0
+0.20.0
Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/examples/simple/get_id.rb 
new/examples/simple/get_id.rb
--- old/examples/simple/get_id.rb       2023-01-18 14:34:24.000000000 +0100
+++ new/examples/simple/get_id.rb       2023-03-21 16:51:02.000000000 +0100
@@ -6,7 +6,9 @@
 
 require "dbus"
 
-bus = DBus::SystemBus.instance
+busname = ARGV.fetch(0, "system")
+bus = busname == "session" ? DBus::SessionBus.instance : 
DBus::SystemBus.instance
+
 driver_svc = bus["org.freedesktop.DBus"]
 # p driver_svc
 driver_obj = driver_svc["/"]
@@ -15,4 +17,4 @@
 # p driver_ifc
 
 bus_id = driver_ifc.GetId
-puts "The system bus id is #{bus_id}"
+puts "The #{busname} bus id is #{bus_id}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/dbus/auth.rb new/lib/dbus/auth.rb
--- old/lib/dbus/auth.rb        2023-01-18 14:34:24.000000000 +0100
+++ new/lib/dbus/auth.rb        2023-03-21 16:51:02.000000000 +0100
@@ -12,261 +12,348 @@
 
 module DBus
   # Exception raised when authentication fails somehow.
-  class AuthenticationFailed < Exception
+  class AuthenticationFailed < StandardError
   end
 
-  # = General class for authentication.
-  class Authenticator
-    # Returns the name of the authenticator.
-    def name
-      self.class.to_s.upcase.sub(/.*::/, "")
+  # The Authentication Protocol.
+  # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
+  #
+  # @api private
+  module Authentication
+    # Base class of authentication mechanisms
+    class Mechanism
+      # @!method call(challenge)
+      # @abstract
+      # Replies to server *challenge*, or sends an initial response if the 
challenge is `nil`.
+      # @param challenge [String,nil]
+      # @return [Array(Symbol,String)] pair [action, response], where
+      #   - [:MechContinue, response] caller should send "DATA response" and 
go to :WaitingForData
+      #   - [:MechOk,       response] caller should send "DATA response" and 
go to :WaitingForOk
+      #   - [:MechError,    message]  caller should send "ERROR message" and 
go to :WaitingForData
+
+      # Uppercase mechanism name, as sent to the server
+      # @return [String]
+      def name
+        self.class.to_s.upcase.sub(/.*::/, "")
+      end
     end
-  end
 
-  # = Anonymous authentication class
-  class Anonymous < Authenticator
-    def authenticate
-      "527562792044427573" # Hex encoded version of "Ruby DBus"
+    # Anonymous authentication class.
+    # 
https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-anonymous
+    class Anonymous < Mechanism
+      def call(_challenge)
+        [:MechOk, "Ruby DBus"]
+      end
     end
-  end
 
-  # = External authentication class
-  #
-  # Class for 'external' type authentication.
-  class External < Authenticator
-    # Performs the authentication.
-    def authenticate
-      # Take the user id (eg integer 1000) make a string out of it "1000", take
-      # each character and determin hex value "1" => 0x31, "0" => 0x30. You
-      # obtain for "1000" => 31303030 This is what the server is expecting.
-      # Why? I dunno. How did I come to that conclusion? by looking at rbus
-      # code. I have no idea how he found that out.
-      Process.uid.to_s.split(//).map { |d| d.ord.to_s(16) }.join
+    # Class for 'external' type authentication.
+    # 
https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-external
+    class External < Mechanism
+      # Performs the authentication.
+      def call(_challenge)
+        [:MechOk, Process.uid.to_s]
+      end
     end
-  end
 
-  # = Authentication class using SHA1 crypto algorithm
-  #
-  # Class for 'CookieSHA1' type authentication.
-  # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
-  class DBusCookieSHA1 < Authenticator
-    # the autenticate method (called in stage one of authentification)
-    def authenticate
-      require "etc"
-      # number of retries we have for auth
-      @retries = 1
-      hex_encode(Etc.getlogin).to_s # server expects it to be binary
-    end
+    # A variant of EXTERNAL that doesn't say our UID.
+    # Seen busctl do this and it worked across a container boundary.
+    class ExternalWithoutUid < External
+      def name
+        "EXTERNAL"
+      end
 
-    # returns the modules name
-    def name
-      "DBUS_COOKIE_SHA1"
+      def call(_challenge)
+        [:MechContinue, nil]
+      end
     end
 
-    # handles the interesting crypto stuff, check the rbus-project for more 
info: http://rbus.rubyforge.org/
-    def data(hexdata)
-      require "digest/sha1"
-      data = hex_decode(hexdata)
-      # name of cookie file, id of cookie in file, servers random challenge
-      context, id, s_challenge = data.split(" ")
-      # Random client challenge
-      c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s 
}.join
-      # Search cookie file for id
-      path = File.join(ENV["HOME"], ".dbus-keyrings", context)
-      DBus.logger.debug "path: #{path.inspect}"
-      File.foreach(path) do |line|
-        if line.start_with?(id)
-          # Right line of file, read cookie
-          cookie = line.split(" ")[2].chomp
-          DBus.logger.debug "cookie: #{cookie.inspect}"
-          # Concatenate and encrypt
-          to_encrypt = [s_challenge, c_challenge, cookie].join(":")
-          sha = Digest::SHA1.hexdigest(to_encrypt)
-          # the almighty tcp server wants everything hex encoded
-          hex_response = hex_encode("#{c_challenge} #{sha}")
-          # Return response
-          response = [:AuthOk, hex_response]
-          return response
-        end
+    # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
+    # 
https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha
+    class DBusCookieSHA1 < Mechanism
+      # returns the modules name
+      def name
+        "DBUS_COOKIE_SHA1"
       end
-      return if @retries <= 0
 
-      # a little rescue magic
-      puts "ERROR: Could not auth, will now exit."
-      puts "ERROR: Unable to locate cookie, retry in 1 second."
-      @retries -= 1
-      sleep 1
-      data(hexdata)
-    end
+      # First we are called with nil and we reply with our username.
+      # Then we prove that we can read that user's cookie file.
+      def call(challenge)
+        if challenge.nil?
+          require "etc"
+          # number of retries we have for auth
+          @retries = 1
+          return [:MechContinue, Etc.getlogin]
+        end
 
-    # encode plain to hex
-    def hex_encode(plain)
-      return nil if plain.nil?
+        require "digest/sha1"
+        # name of cookie file, id of cookie in file, servers random challenge
+        context, id, s_challenge = challenge.split(" ")
+        # Random client challenge
+        c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s 
}.join
+        # Search cookie file for id
+        path = File.join(ENV["HOME"], ".dbus-keyrings", context)
+        DBus.logger.debug "path: #{path.inspect}"
+        File.foreach(path) do |line|
+          if line.start_with?(id)
+            # Right line of file, read cookie
+            cookie = line.split(" ")[2].chomp
+            DBus.logger.debug "cookie: #{cookie.inspect}"
+            # Concatenate and encrypt
+            to_encrypt = [s_challenge, c_challenge, cookie].join(":")
+            sha = Digest::SHA1.hexdigest(to_encrypt)
+            # Return response
+            response = [:MechOk, "#{c_challenge} #{sha}"]
+            return response
+          end
+        end
+        return if @retries <= 0
 
-      plain.to_s.unpack1("H*")
+        # a little rescue magic
+        puts "ERROR: Could not auth, will now exit."
+        puts "ERROR: Unable to locate cookie, retry in 1 second."
+        @retries -= 1
+        sleep 1
+        call(challenge)
+      end
     end
 
-    # decode hex to plain
-    def hex_decode(encoded)
-      encoded.scan(/[[:xdigit:]]{2}/).map { |h| h.hex.chr }.join
-    end
-  end
+    # Declare client state transitions, for ease of code reading.
+    # It is just a pair.
+    NextState = Struct.new(:state, :command_words)
+
+    # Authenticates the connection before messages can be exchanged.
+    class Client
+      # @return [Boolean] have we negotiated Unix file descriptor passing
+      # NOTE: not implemented yet in upper layers
+      attr_reader :unix_fd
+
+      # @return [String]
+      attr_reader :address_uuid
+
+      # Create a new authentication client.
+      # @param mechs [Array<Mechanism,Class>,nil] custom list of auth 
Mechanism objects or classes
+      def initialize(socket, mechs = nil)
+        @unix_fd = false
+        @address_uuid = nil
+
+        @socket = socket
+        @state = nil
+        @auth_list = mechs || [
+          External,
+          DBusCookieSHA1,
+          ExternalWithoutUid,
+          Anonymous
+        ]
+      end
 
-  # Note: this following stuff is tested with External authenticator only!
+      # Start the authentication process.
+      # @return [void]
+      # @raise [AuthenticationFailed]
+      def authenticate
+        DBus.logger.debug "Authenticating"
+        send_nul_byte
+
+        use_next_mechanism
+
+        @state, command = next_state_via_mechanism.to_a
+        send(command)
+
+        loop do
+          DBus.logger.debug "auth STATE: #{@state}"
+          words = next_msg
 
-  # = Authentication client class.
-  #
-  # Class tha performs the actional authentication.
-  class Client
-    # Create a new authentication client.
-    def initialize(socket)
-      @socket = socket
-      @state = nil
-      @auth_list = [External, DBusCookieSHA1, Anonymous]
-    end
+          @state, command = next_state(words).to_a
+          break if [:TerminatedOk, :TerminatedError].include? @state
+
+          send(command)
+        end
 
-    # Start the authentication process.
-    def authenticate
-      if RbConfig::CONFIG["target_os"] =~ /freebsd/
-        @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
-      else
-        @socket.write(0.chr)
-      end
-      next_authenticator
-      @state = :Starting
-      while @state != :Authenticated
-        r = next_state
-        return r if !r
+        raise AuthenticationFailed, command.first if @state == :TerminatedError
+
+        send("BEGIN")
       end
-      true
-    end
 
-    ##########
+      ##########
 
-    private
+      private
 
-    ##########
+      ##########
 
-    # Send an authentication method _meth_ with arguments _args_ to the
-    # server.
-    def send(meth, *args)
-      o = ([meth] + args).join(" ")
-      @socket.write("#{o}\r\n")
-    end
+      # The authentication protocol requires a nul byte
+      # that may carry credentials.
+      # @return [void]
+      def send_nul_byte
+        if RbConfig::CONFIG["target_os"] =~ /freebsd/
+          @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
+        else
+          @socket.write(0.chr)
+        end
+      end
 
-    # Try authentication using the next authenticator.
-    def next_authenticator
-      raise AuthenticationFailed if @auth_list.empty?
-
-      @authenticator = @auth_list.shift.new
-      auth_msg = ["AUTH", @authenticator.name, @authenticator.authenticate]
-      DBus.logger.debug "auth_msg: #{auth_msg.inspect}"
-      send(auth_msg)
-    rescue AuthenticationFailed
-      @socket.close
-      raise
-    end
+      # encode plain to hex
+      # @param plain [String,nil]
+      # @return [String,nil]
+      def hex_encode(plain)
+        return nil if plain.nil?
 
-    # Read data (a buffer) from the bus until CR LF is encountered.
-    # Return the buffer without the CR LF characters.
-    def next_msg
-      data = ""
-      crlf = "\r\n"
-      left = 1024 # 1024 byte, no idea if it's ever getting bigger
-      while left.positive?
-        buf = @socket.read(left > 1 ? 1 : left)
-        break if buf.nil?
-
-        left -= buf.bytesize
-        data += buf
-        break if data.include? crlf # crlf means line finished, the TCP socket 
keeps on listening, so we break
-      end
-      readline = data.chomp.split(" ")
-      DBus.logger.debug "readline: #{readline.inspect}"
-      readline
-    end
+        plain.unpack1("H*")
+      end
 
-    #     # Read data (a buffer) from the bus until CR LF is encountered.
-    #     # Return the buffer without the CR LF characters.
-    #     def next_msg
-    #       @socket.readline.chomp.split(" ")
-    #     end
-
-    # Try to reach the next state based on the current state.
-    def next_state
-      msg = next_msg
-      if @state == :Starting
-        DBus.logger.debug ":Starting msg: #{msg[0].inspect}"
-        case msg[0]
-        when "OK"
-          @state = :WaitingForOk
-        when "CONTINUE"
-          @state = :WaitingForData
-        when "REJECTED" # needed by tcp, unix-path/abstract doesn't get here
-          @state = :WaitingForData
-        end
+      # decode hex to plain
+      # @param encoded [String,nil]
+      # @return [String,nil]
+      def hex_decode(encoded)
+        return nil if encoded.nil?
+
+        [encoded].pack("H*")
       end
-      DBus.logger.debug "state: #{@state}"
-      case @state
-      when :WaitingForData
-        DBus.logger.debug ":WaitingForData msg: #{msg[0].inspect}"
-        case msg[0]
-        when "DATA"
-          chall = msg[1]
-          resp, chall = @authenticator.data(chall)
-          DBus.logger.debug ":WaitingForData/DATA resp: #{resp.inspect}"
-          case resp
-          when :AuthContinue
-            send("DATA", chall)
-            @state = :WaitingForData
-          when :AuthOk
-            send("DATA", chall)
-            @state = :WaitingForOk
-          when :AuthError
-            send("ERROR")
-            @state = :WaitingForData
-          end
-        when "REJECTED"
-          next_authenticator
-          @state = :WaitingForData
-        when "ERROR"
-          send("CANCEL")
-          @state = :WaitingForReject
-        when "OK"
-          send("BEGIN")
-          @state = :Authenticated
-        else
-          send("ERROR")
-          @state = :WaitingForData
+
+      # Send a string to the socket; good place for test mocks.
+      def write_line(str)
+        DBus.logger.debug "auth_write: #{str.inspect}"
+        @socket.write(str)
+      end
+
+      # Send *words* to the server as a single CRLF terminated string.
+      # @param words [Array<String>,String]
+      def send(words)
+        joined = Array(words).compact.join(" ")
+        write_line("#{joined}\r\n")
+      end
+
+      # Try authentication using the next mechanism.
+      # @raise [AuthenticationFailed] if there are no more left
+      # @return [void]
+      def use_next_mechanism
+        raise AuthenticationFailed, "Authentication mechanisms exhausted" if 
@auth_list.empty?
+
+        @mechanism = @auth_list.shift
+        @mechanism = @mechanism.new if @mechanism.is_a? Class
+      rescue AuthenticationFailed
+        # TODO: make this caller's responsibility
+        @socket.close
+        raise
+      end
+
+      # Read data (a buffer) from the bus until CR LF is encountered.
+      # Return the buffer without the CR LF characters.
+      # @return [Array<String>] received words
+      def next_msg
+        read_line.chomp.split(" ")
+      end
+
+      # Read a line from the socket; good place for test mocks.
+      # @return [String] CRLF (\r\n) terminated
+      def read_line
+        # TODO: probably can simply call @socket.readline
+        data = ""
+        crlf = "\r\n"
+        left = 1024 # 1024 byte, no idea if it's ever getting bigger
+        while left.positive?
+          buf = @socket.read(left > 1 ? 1 : left)
+          break if buf.nil?
+
+          left -= buf.bytesize
+          data += buf
+          break if data.include? crlf # crlf means line finished, the TCP 
socket keeps on listening, so we break
         end
-      when :WaitingForOk
-        DBus.logger.debug ":WaitingForOk msg: #{msg[0].inspect}"
-        case msg[0]
-        when "OK"
-          send("BEGIN")
-          @state = :Authenticated
-        when "REJECT"
-          next_authenticator
-          @state = :WaitingForData
-        when "DATA", "ERROR"
-          send("CANCEL")
-          @state = :WaitingForReject
+        DBus.logger.debug "auth_read: #{data.inspect}"
+        data
+      end
+
+      #     # Read data (a buffer) from the bus until CR LF is encountered.
+      #     # Return the buffer without the CR LF characters.
+      #     def next_msg
+      #       @socket.readline.chomp.split(" ")
+      #     end
+
+      # @param hex_challenge [String,nil] (nil when the server said "DATA\r\n")
+      # @param use_data [Boolean] say DATA instead of AUTH
+      # @return [NextState]
+      def next_state_via_mechanism(hex_challenge = nil, use_data: false)
+        challenge = hex_decode(hex_challenge)
+
+        action, response = @mechanism.call(challenge)
+        DBus.logger.debug "auth mechanism action: #{action.inspect}"
+
+        command = use_data ? ["DATA"] : ["AUTH", @mechanism.name]
+
+        case action
+        when :MechError
+          NextState.new(:WaitingForData, ["ERROR", response])
+        when :MechContinue
+          NextState.new(:WaitingForData, command + [hex_encode(response)])
+        when :MechOk
+          NextState.new(:WaitingForOk, command + [hex_encode(response)])
         else
-          send("ERROR")
-          @state = :WaitingForOk
+          raise AuthenticationFailed, "internal error, unknown action 
#{action.inspect} " \
+                                      "from our mechanism 
#{@mechanism.inspect}"
         end
-      when :WaitingForReject
-        DBus.logger.debug ":WaitingForReject msg: #{msg[0].inspect}"
-        case msg[0]
-        when "REJECT"
-          next_authenticator
-          @state = :WaitingForOk
+      end
+
+      # Try to reach the next state based on the current state.
+      # @param received_words [Array<String>]
+      # @return [NextState]
+      def next_state(received_words)
+        msg = received_words
+
+        case @state
+        when :WaitingForData
+          case msg[0]
+          when "DATA"
+            next_state_via_mechanism(msg[1], use_data: true)
+          when "REJECTED"
+            use_next_mechanism
+            next_state_via_mechanism
+          when "ERROR"
+            NextState.new(:WaitingForReject, ["CANCEL"])
+          when "OK"
+            @address_uuid = msg[1]
+            # NextState.new(:TerminatedOk, [])
+            NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
+          else
+            NextState.new(:WaitingForData, ["ERROR"])
+          end
+        when :WaitingForOk
+          case msg[0]
+          when "OK"
+            @address_uuid = msg[1]
+            # NextState.new(:TerminatedOk, [])
+            NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
+          when "REJECTED"
+            use_next_mechanism
+            next_state_via_mechanism
+          when "DATA", "ERROR"
+            NextState.new(:WaitingForReject, ["CANCEL"])
+          else
+            # we don't understand server's response but still wait for a 
successful auth completion
+            NextState.new(:WaitingForOk, ["ERROR"])
+          end
+        when :WaitingForReject
+          case msg[0]
+          when "REJECTED"
+            use_next_mechanism
+            next_state_via_mechanism
+          else
+            # TODO: spec says to close socket, clarify
+            NextState.new(:TerminatedError, ["Unknown server reply 
#{msg[0].inspect} when expecting REJECTED"])
+          end
+        when :WaitingForAgreeUnixFD
+          case msg[0]
+          when "AGREE_UNIX_FD"
+            @unix_fd = true
+            NextState.new(:TerminatedOk, [])
+          when "ERROR"
+            @unix_fd = false
+            NextState.new(:TerminatedOk, [])
+          else
+            # TODO: spec says to close socket, clarify
+            NextState.new(:TerminatedError, ["Unknown server reply 
#{msg[0].inspect} to NEGOTIATE_UNIX_FD"])
+          end
         else
-          @socket.close
-          return false
+          raise "Internal error: unhandled state #{@state.inspect}"
         end
       end
-      true
     end
   end
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/dbus/message_queue.rb 
new/lib/dbus/message_queue.rb
--- old/lib/dbus/message_queue.rb       2023-01-18 14:34:24.000000000 +0100
+++ new/lib/dbus/message_queue.rb       2023-03-21 16:51:02.000000000 +0100
@@ -19,9 +19,11 @@
     attr_reader :socket
 
     def initialize(address)
+      DBus.logger.debug "MessageQueue: #{address}"
       @address = address
       @buffer = ""
       @is_tcp = false
+      @mutex = Mutex.new
       connect
     end
 
@@ -32,23 +34,28 @@
     # @raise EOFError
     # @todo failure modes
     def pop(blocking: true)
-      buffer_from_socket_nonblock
-      message = message_from_buffer_nonblock
-      if blocking
-        # we can block
-        while message.nil?
-          r, _d, _d = IO.select([@socket])
-          if r && r[0] == @socket
-            buffer_from_socket_nonblock
-            message = message_from_buffer_nonblock
+      # FIXME: this is not enough, the R/W test deadlocks on shared connections
+      @mutex.synchronize do
+        buffer_from_socket_nonblock
+        message = message_from_buffer_nonblock
+        if blocking
+          # we can block
+          while message.nil?
+            r, _d, _d = IO.select([@socket])
+            if r && r[0] == @socket
+              buffer_from_socket_nonblock
+              message = message_from_buffer_nonblock
+            end
           end
         end
+        message
       end
-      message
     end
 
     def push(message)
-      @socket.write(message.marshall)
+      @mutex.synchronize do
+        @socket.write(message.marshall)
+      end
     end
     alias << push
 
@@ -129,7 +136,7 @@
 
     # Initialize the connection to the bus.
     def init_connection
-      client = Client.new(@socket)
+      client = Authentication::Client.new(@socket)
       client.authenticate
     end
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/metadata new/metadata
--- old/metadata        2023-01-18 14:34:25.000000000 +0100
+++ new/metadata        2023-03-21 16:51:02.000000000 +0100
@@ -1,14 +1,14 @@
 --- !ruby/object:Gem::Specification
 name: ruby-dbus
 version: !ruby/object:Gem::Version
-  version: 0.19.0
+  version: 0.20.0
 platform: ruby
 authors:
 - Ruby DBus Team
 autorequire: 
 bindir: bin
 cert_chain: []
-date: 2023-01-18 00:00:00.000000000 Z
+date: 2023-03-21 00:00:00.000000000 Z
 dependencies:
 - !ruby/object:Gem::Dependency
   name: rexml
@@ -165,6 +165,7 @@
 - lib/dbus/xml.rb
 - ruby-dbus.gemspec
 - spec/async_spec.rb
+- spec/auth_spec.rb
 - spec/binding_spec.rb
 - spec/bus_and_xml_backend_spec.rb
 - spec/bus_driver_spec.rb
@@ -186,6 +187,7 @@
 - spec/packet_marshaller_spec.rb
 - spec/packet_unmarshaller_spec.rb
 - spec/property_spec.rb
+- spec/proxy_object_interface_spec.rb
 - spec/proxy_object_spec.rb
 - spec/server_robustness_spec.rb
 - spec/server_spec.rb
@@ -223,8 +225,7 @@
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
-rubyforge_project: 
-rubygems_version: 2.7.6.3
+rubygems_version: 3.3.0.dev
 signing_key: 
 specification_version: 4
 summary: Ruby module for interaction with D-Bus
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/spec/auth_spec.rb new/spec/auth_spec.rb
--- old/spec/auth_spec.rb       1970-01-01 01:00:00.000000000 +0100
+++ new/spec/auth_spec.rb       2023-03-21 16:51:02.000000000 +0100
@@ -0,0 +1,225 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::Authentication::Client do
+  let(:socket) { instance_double("Socket") }
+  let(:subject) { described_class.new(socket) }
+
+  before(:each) do
+    allow(Process).to receive(:uid).and_return(999)
+    allow(subject).to receive(:send_nul_byte)
+  end
+
+  describe "#next_state" do
+    it "raises when I forget to handle a state" do
+      subject.instance_variable_set(:@state, :Denmark)
+      expect { subject.__send__(:next_state, []) }.to 
raise_error(RuntimeError, /unhandled state :Denmark/)
+    end
+  end
+
+  def expect_protocol(pairs)
+    pairs.each do |we_say, server_says|
+      expect(subject).to receive(:write_line).with(we_say)
+      next if server_says.nil?
+
+      expect(subject).to receive(:read_line).and_return(server_says)
+    end
+  end
+
+  context "with ANONYMOUS" do
+    let(:subject) { described_class.new(socket, 
[DBus::Authentication::Anonymous]) }
+
+    it "authentication passes" do
+      expect_protocol [
+        ["AUTH ANONYMOUS 527562792044427573\r\n", "OK 
ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "ERROR not for anonymous\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with EXTERNAL" do
+    let(:subject) { described_class.new(socket, 
[DBus::Authentication::External]) }
+
+    it "authentication passes, and address_uuid is set" do
+      expect_protocol [
+        ["AUTH EXTERNAL 393939\r\n", "OK 
ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+      expect(subject.address_uuid).to eq "ffffffffffffffffffffffffffffffff"
+    end
+
+    context "when the server says superfluous things before an OK" do
+      it "authentication passes" do
+        expect_protocol [
+          ["AUTH EXTERNAL 393939\r\n", "WOULD_YOU_LIKE_SOME_TEA\r\n"],
+          ["ERROR\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+          ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+          ["BEGIN\r\n"]
+        ]
+
+        expect { subject.authenticate }.to_not raise_error
+      end
+    end
+
+    context "when the server messes up NEGOTIATE_UNIX_FD" do
+      it "authentication fails orderly" do
+        expect_protocol [
+          ["AUTH EXTERNAL 393939\r\n", "OK 
ffffffffffffffffffffffffffffffff\r\n"],
+          ["NEGOTIATE_UNIX_FD\r\n", "I_DONT_NEGOTIATE_WITH_TENORISTS\r\n"]
+        ]
+
+        allow(socket).to receive(:close) # want to get rid of this
+        # TODO: quote the server error message?
+        expect { subject.authenticate }.to 
raise_error(DBus::AuthenticationFailed, /Unknown server reply/)
+      end
+    end
+
+    context "when the server replies with ERROR" do
+      it "authentication fails orderly" do
+        expect_protocol [
+          ["AUTH EXTERNAL 393939\r\n", "ERROR something failed\r\n"],
+          ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
+        ]
+
+        allow(socket).to receive(:close) # want to get rid of this
+        # TODO: quote the server error message?
+        expect { subject.authenticate }.to 
raise_error(DBus::AuthenticationFailed, /exhausted/)
+      end
+    end
+  end
+
+  context "with EXTERNAL without uid" do
+    let(:subject) do
+      described_class.new(socket, [DBus::Authentication::External, 
DBus::Authentication::ExternalWithoutUid])
+    end
+
+    it "authentication passes" do
+      expect_protocol [
+        ["AUTH EXTERNAL 393939\r\n", "REJECTED EXTERNAL\r\n"],
+        # this succeeds when we connect to a privileged container,
+        # where outside-non-root becomes inside-root
+        ["AUTH EXTERNAL\r\n", "DATA\r\n"],
+        ["DATA\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with a rejected mechanism and then EXTERNAL" do
+    let(:rejected_mechanism) do
+      double("Mechanism", name: "WIMP", call: [:MechContinue, "I expect to be 
rejected"])
+    end
+
+    let(:subject) { described_class.new(socket, [rejected_mechanism, 
DBus::Authentication::External]) }
+
+    it "authentication eventually passes" do
+      expect_protocol [
+        [/^AUTH WIMP .*\r\n/, "REJECTED EXTERNAL\r\n"],
+        ["AUTH EXTERNAL 393939\r\n", "OK 
ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with a DATA-using mechanism" do
+    let(:mechanism) do
+      double("Mechanism", name: "CHALLENGE_ME", call: [:MechContinue, "1"])
+    end
+
+    # try it twice to test calling #use_next_mechanism
+    let(:subject) { described_class.new(socket, [mechanism, mechanism]) }
+
+    it "authentication fails orderly when the server says ERROR" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
+        ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"],
+        ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
+        ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
+      ]
+
+      allow(socket).to receive(:close) # want to get rid of this
+      # TODO: quote the server error message?
+      expect { subject.authenticate }.to 
raise_error(DBus::AuthenticationFailed, /exhausted/)
+    end
+
+    it "authentication fails orderly when the server says ERROR and then 
changes its mind" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
+        ["CANCEL\r\n", "I_CHANGED_MY_MIND please come back\r\n"]
+      ]
+
+      allow(socket).to receive(:close) # want to get rid of this
+      # TODO: quote the server error message?
+      expect { subject.authenticate }.to 
raise_error(DBus::AuthenticationFailed, /Unknown.*MIND.*REJECTED/)
+    end
+
+    it "authentication passes when the server says superfluous things before 
DATA" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "WOULD_YOU_LIKE_SOME_TEA\r\n"],
+        ["ERROR\r\n", "DATA\r\n"],
+        ["DATA 31\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+
+    it "authentication passes when the server decides not to need the DATA" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "OK 
ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with a mechanism returning :MechError" do
+    let(:fallible_mechanism) do
+      double(name: "FALLIBLE", call: [:MechError, "not my best day"])
+    end
+
+    let(:subject) { described_class.new(socket, [fallible_mechanism]) }
+
+    it "authentication fails orderly" do
+      expect_protocol [
+        ["ERROR not my best day\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
+      ]
+
+      allow(socket).to receive(:close) # want to get rid of thise
+      expect { subject.authenticate }.to 
raise_error(DBus::AuthenticationFailed, /exhausted/)
+    end
+  end
+
+  context "with a badly implemented mechanism" do
+    let(:buggy_mechanism) do
+      double(name: "buggy", call: [:smurf, nil])
+    end
+
+    let(:subject) { described_class.new(socket, [buggy_mechanism]) }
+
+    it "authentication fails before protoxol is exchanged" do
+      expect(subject).to_not receive(:write_line)
+      expect(subject).to_not receive(:read_line)
+
+      expect { subject.authenticate }.to 
raise_error(DBus::AuthenticationFailed, /smurf/)
+    end
+  end
+end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/spec/proxy_object_interface_spec.rb 
new/spec/proxy_object_interface_spec.rb
--- old/spec/proxy_object_interface_spec.rb     1970-01-01 01:00:00.000000000 
+0100
+++ new/spec/proxy_object_interface_spec.rb     2023-03-21 16:51:02.000000000 
+0100
@@ -0,0 +1,35 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::ProxyObjectInterface do
+  # TODO: tag tests that need a service, eg "needs-service"
+  # TODO: maybe remove this and rely on a packaged tool
+  around(:each) do |example|
+    with_private_bus do
+      with_service_by_activation(&example)
+    end
+  end
+
+  let(:bus) { DBus::ASessionBus.new }
+
+  context "when calling org.ruby.service" do
+    let(:svc) { bus["org.ruby.service"] }
+
+    # This is white box testing, knowing the implementation
+    # A better way would be structuring it according to the D-Bus Spec
+    # Or testing the service side doing the right thing? (What if our bugs 
cancel out)
+    describe "#define_method_from_descriptor" do
+      it "can call a method with multiple OUT arguments" do
+        obj = svc["/org/ruby/MyInstance"]
+        ifc = obj["org.ruby.SampleInterface"]
+
+        even, odd = ifc.EvenOdd([3, 1, 4, 1, 5, 9, 2, 6])
+        expect(even).to eq [4, 2, 6]
+        expect(odd).to eq [3, 1, 1, 5, 9]
+      end
+    end
+  end
+end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/spec/service_newapi.rb new/spec/service_newapi.rb
--- old/spec/service_newapi.rb  2023-01-18 14:34:24.000000000 +0100
+++ new/spec/service_newapi.rb  2023-03-21 16:51:02.000000000 +0100
@@ -102,6 +102,12 @@
       [coords]
     end
 
+    # Two OUT arguments
+    dbus_method :EvenOdd, "in numbers:ai, out even:ai, out odd:ai" do |numbers|
+      even, odd = numbers.partition(&:even?)
+      [even, odd]
+    end
+
     # Properties:
     # ReadMe:string, returns "READ ME" at first, then what WriteMe received
     # WriteMe:string
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/spec/spec_helper.rb new/spec/spec_helper.rb
--- old/spec/spec_helper.rb     2023-01-18 14:34:24.000000000 +0100
+++ new/spec/spec_helper.rb     2023-03-21 16:51:02.000000000 +0100
@@ -31,7 +31,7 @@
       c.single_report_path = "coverage/lcov.info"
     end
 
-    SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
+    SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [
       SimpleCov::Formatter::HTMLFormatter,
       SimpleCov::Formatter::LcovFormatter
     ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/spec/thread_safety_spec.rb 
new/spec/thread_safety_spec.rb
--- old/spec/thread_safety_spec.rb      2023-01-18 14:34:24.000000000 +0100
+++ new/spec/thread_safety_spec.rb      2023-03-21 16:51:02.000000000 +0100
@@ -5,28 +5,71 @@
 require_relative "spec_helper"
 require "dbus"
 
-describe "ThreadSafetyTest" do
-  it "tests thread competition" do
-    print "Thread competition: "
-    jobs = []
-    5.times do
-      jobs << Thread.new do
-        Thread.current.abort_on_exception = true
+class TestSignalRace < DBus::Object
+  dbus_interface "org.ruby.ServerTest" do
+    dbus_signal :signal_without_arguments
+  end
+end
+
+# Run *count* threads all doing *body*, wait for their finish
+def race_threads(count, &body)
+  jobs = count.times.map do |j|
+    Thread.new do
+      Thread.current.abort_on_exception = true
+
+      body.call(j)
+    end
+  end
+  jobs.each(&:join)
+end
+
+# Repeat *count* times: { random sleep, *body* }, printing progress
+def repeat_with_jitter(count, &body)
+  count.times do |i|
+    sleep 0.1 * rand
+    print "#{i} "
+    $stdout.flush
 
+    body.call
+  end
+end
+
+describe "thread safety" do
+  context "R/W: when the threads call methods with return values" do
+    it "it works with separate bus connections" do
+      race_threads(5) do |_j|
         # use separate connections to avoid races
         bus = DBus::ASessionBus.new
         svc = bus.service("org.ruby.service")
         obj = svc.object("/org/ruby/MyInstance")
         obj.default_iface = "org.ruby.SampleInterface"
 
-        10.times do |i|
-          print "#{i} "
-          $stdout.flush
+        repeat_with_jitter(10) do
           expect(obj.the_answer[0]).to eq(42)
-          sleep 0.1 * rand
         end
       end
+      puts
+    end
+  end
+
+  context "W/O: when the threads only send signals" do
+    it "it works with a shared separate bus connection" do
+      race_threads(5) do |j|
+        # shared connection
+        bus = DBus::SessionBus.instance
+        # hackish: we do not actually request the name
+        svc = DBus::Service.new("org.ruby.server-test#{j}", bus)
+
+        obj = TestSignalRace.new "/org/ruby/Foo"
+        svc.export obj
+
+        repeat_with_jitter(10) do
+          obj.signal_without_arguments
+        end
+
+        svc.unexport(obj)
+      end
+      puts
     end
-    jobs.each(&:join)
   end
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/spec/tools/dbus-limited-session.conf 
new/spec/tools/dbus-limited-session.conf
--- old/spec/tools/dbus-limited-session.conf    2023-01-18 14:34:24.000000000 
+0100
+++ new/spec/tools/dbus-limited-session.conf    2023-03-21 16:51:02.000000000 
+0100
@@ -7,6 +7,20 @@
   <!-- Our well-known bus type, don't change this -->
   <type>session</type>
 
+  <!-- Authentication:
+       This was useful during refactoring, but meanwhile RSpec mocking has
+       replaced it. -->
+  <!-- Explicitly list all known authentication mechanisms,
+       their order is not important.
+       By default the daemon allows all but this lets me disable some. -->
+  <auth>EXTERNAL</auth>
+  <auth>DBUS_COOKIE_SHA1</auth>
+  <auth>ANONYMOUS</auth>
+  <!-- Insecure, other users could call us and exploit debug APIs/bugs -->
+  <!--
+  <allow_anonymous/>
+  -->
+
   <listen>unix:tmpdir=/tmp</listen>
   <listen>tcp:host=127.0.0.1</listen>
 

Reply via email to