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>