ruby-dbus 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7261c5ad0fb4e067da97b31298e6a79023e54743f69563b22c9a751101df2f85
4
- data.tar.gz: 38075d862b0407889bf0a0e25eff450d18c8e225012a028545700fc1be15c342
3
+ metadata.gz: d52907f5f6be32813466ae280cb99ff605d4195ca84396e97a90a7568fb579ec
4
+ data.tar.gz: 01bf87805f19878641a424f5c9ba21fcff637dcb6d4bbcdc230dda3713df8c80
5
5
  SHA512:
6
- metadata.gz: 700dc07af8d28f7a7537be292dd6313268032f169102c3581d2f5c7a13ba8b9b7c864b23d3b8bdb66e3760a147a7edf7d835098f450625ffade7f748e101f987
7
- data.tar.gz: 2637cb7cad666a415c6e4a83a158b116507e5d6db3e70a53f5384d1a9240f191e6e2b540950076ae76397b7581c891e71b9f59e84346647394fe0bd7833e49c1
6
+ metadata.gz: 8009bcca0c66ddb5d5e9ca76a2fbc06b0408437ac3f5135d1378362f58520cbcb20c6d625d242e7c19ca1f679b4ab7b4e584e5f9d6c376eea90f57372f7f27aa
7
+ data.tar.gz: 0d8388654ae2ad5aeca7cf0eb6cec2fb3eb463ac43b85eb7a83bfbd32a011d65749b1a1195fcc4ae0f8c6a5591e49c0176a09c442c67a166ce2432bb30d704f7
data/NEWS.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## Ruby D-Bus 0.20.0 - 2023-03-21
6
+
7
+ Features:
8
+ * For EXTERNAL authentication, try also without the user id, to work with
9
+ containers ([#126][]).
10
+ * Thread safety, as long as the non-main threads only send signals.
11
+
12
+ [#126]: https://github.com/mvidner/ruby-dbus/issues/126
13
+
5
14
  ## Ruby D-Bus 0.19.0 - 2023-01-18
6
15
 
7
16
  API:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.19.0
1
+ 0.20.0
@@ -6,7 +6,9 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
6
6
 
7
7
  require "dbus"
8
8
 
9
- bus = DBus::SystemBus.instance
9
+ busname = ARGV.fetch(0, "system")
10
+ bus = busname == "session" ? DBus::SessionBus.instance : DBus::SystemBus.instance
11
+
10
12
  driver_svc = bus["org.freedesktop.DBus"]
11
13
  # p driver_svc
12
14
  driver_obj = driver_svc["/"]
@@ -15,4 +17,4 @@ driver_ifc = driver_obj["org.freedesktop.DBus"]
15
17
  # p driver_ifc
16
18
 
17
19
  bus_id = driver_ifc.GetId
18
- puts "The system bus id is #{bus_id}"
20
+ puts "The #{busname} bus id is #{bus_id}"
data/lib/dbus/auth.rb CHANGED
@@ -12,261 +12,348 @@ require "rbconfig"
12
12
 
13
13
  module DBus
14
14
  # Exception raised when authentication fails somehow.
15
- class AuthenticationFailed < Exception
15
+ class AuthenticationFailed < StandardError
16
16
  end
17
17
 
18
- # = General class for authentication.
19
- class Authenticator
20
- # Returns the name of the authenticator.
21
- def name
22
- self.class.to_s.upcase.sub(/.*::/, "")
23
- end
24
- end
18
+ # The Authentication Protocol.
19
+ # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
20
+ #
21
+ # @api private
22
+ module Authentication
23
+ # Base class of authentication mechanisms
24
+ class Mechanism
25
+ # @!method call(challenge)
26
+ # @abstract
27
+ # Replies to server *challenge*, or sends an initial response if the challenge is `nil`.
28
+ # @param challenge [String,nil]
29
+ # @return [Array(Symbol,String)] pair [action, response], where
30
+ # - [:MechContinue, response] caller should send "DATA response" and go to :WaitingForData
31
+ # - [:MechOk, response] caller should send "DATA response" and go to :WaitingForOk
32
+ # - [:MechError, message] caller should send "ERROR message" and go to :WaitingForData
25
33
 
26
- # = Anonymous authentication class
27
- class Anonymous < Authenticator
28
- def authenticate
29
- "527562792044427573" # Hex encoded version of "Ruby DBus"
34
+ # Uppercase mechanism name, as sent to the server
35
+ # @return [String]
36
+ def name
37
+ self.class.to_s.upcase.sub(/.*::/, "")
38
+ end
30
39
  end
31
- end
32
40
 
33
- # = External authentication class
34
- #
35
- # Class for 'external' type authentication.
36
- class External < Authenticator
37
- # Performs the authentication.
38
- def authenticate
39
- # Take the user id (eg integer 1000) make a string out of it "1000", take
40
- # each character and determin hex value "1" => 0x31, "0" => 0x30. You
41
- # obtain for "1000" => 31303030 This is what the server is expecting.
42
- # Why? I dunno. How did I come to that conclusion? by looking at rbus
43
- # code. I have no idea how he found that out.
44
- Process.uid.to_s.split(//).map { |d| d.ord.to_s(16) }.join
41
+ # Anonymous authentication class.
42
+ # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-anonymous
43
+ class Anonymous < Mechanism
44
+ def call(_challenge)
45
+ [:MechOk, "Ruby DBus"]
46
+ end
45
47
  end
46
- end
47
48
 
48
- # = Authentication class using SHA1 crypto algorithm
49
- #
50
- # Class for 'CookieSHA1' type authentication.
51
- # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
52
- class DBusCookieSHA1 < Authenticator
53
- # the autenticate method (called in stage one of authentification)
54
- def authenticate
55
- require "etc"
56
- # number of retries we have for auth
57
- @retries = 1
58
- hex_encode(Etc.getlogin).to_s # server expects it to be binary
49
+ # Class for 'external' type authentication.
50
+ # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-external
51
+ class External < Mechanism
52
+ # Performs the authentication.
53
+ def call(_challenge)
54
+ [:MechOk, Process.uid.to_s]
55
+ end
59
56
  end
60
57
 
61
- # returns the modules name
62
- def name
63
- "DBUS_COOKIE_SHA1"
58
+ # A variant of EXTERNAL that doesn't say our UID.
59
+ # Seen busctl do this and it worked across a container boundary.
60
+ class ExternalWithoutUid < External
61
+ def name
62
+ "EXTERNAL"
63
+ end
64
+
65
+ def call(_challenge)
66
+ [:MechContinue, nil]
67
+ end
64
68
  end
65
69
 
66
- # handles the interesting crypto stuff, check the rbus-project for more info: http://rbus.rubyforge.org/
67
- def data(hexdata)
68
- require "digest/sha1"
69
- data = hex_decode(hexdata)
70
- # name of cookie file, id of cookie in file, servers random challenge
71
- context, id, s_challenge = data.split(" ")
72
- # Random client challenge
73
- c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s }.join
74
- # Search cookie file for id
75
- path = File.join(ENV["HOME"], ".dbus-keyrings", context)
76
- DBus.logger.debug "path: #{path.inspect}"
77
- File.foreach(path) do |line|
78
- if line.start_with?(id)
79
- # Right line of file, read cookie
80
- cookie = line.split(" ")[2].chomp
81
- DBus.logger.debug "cookie: #{cookie.inspect}"
82
- # Concatenate and encrypt
83
- to_encrypt = [s_challenge, c_challenge, cookie].join(":")
84
- sha = Digest::SHA1.hexdigest(to_encrypt)
85
- # the almighty tcp server wants everything hex encoded
86
- hex_response = hex_encode("#{c_challenge} #{sha}")
87
- # Return response
88
- response = [:AuthOk, hex_response]
89
- return response
70
+ # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
71
+ # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha
72
+ class DBusCookieSHA1 < Mechanism
73
+ # returns the modules name
74
+ def name
75
+ "DBUS_COOKIE_SHA1"
76
+ end
77
+
78
+ # First we are called with nil and we reply with our username.
79
+ # Then we prove that we can read that user's cookie file.
80
+ def call(challenge)
81
+ if challenge.nil?
82
+ require "etc"
83
+ # number of retries we have for auth
84
+ @retries = 1
85
+ return [:MechContinue, Etc.getlogin]
86
+ end
87
+
88
+ require "digest/sha1"
89
+ # name of cookie file, id of cookie in file, servers random challenge
90
+ context, id, s_challenge = challenge.split(" ")
91
+ # Random client challenge
92
+ c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s }.join
93
+ # Search cookie file for id
94
+ path = File.join(ENV["HOME"], ".dbus-keyrings", context)
95
+ DBus.logger.debug "path: #{path.inspect}"
96
+ File.foreach(path) do |line|
97
+ if line.start_with?(id)
98
+ # Right line of file, read cookie
99
+ cookie = line.split(" ")[2].chomp
100
+ DBus.logger.debug "cookie: #{cookie.inspect}"
101
+ # Concatenate and encrypt
102
+ to_encrypt = [s_challenge, c_challenge, cookie].join(":")
103
+ sha = Digest::SHA1.hexdigest(to_encrypt)
104
+ # Return response
105
+ response = [:MechOk, "#{c_challenge} #{sha}"]
106
+ return response
107
+ end
90
108
  end
109
+ return if @retries <= 0
110
+
111
+ # a little rescue magic
112
+ puts "ERROR: Could not auth, will now exit."
113
+ puts "ERROR: Unable to locate cookie, retry in 1 second."
114
+ @retries -= 1
115
+ sleep 1
116
+ call(challenge)
91
117
  end
92
- return if @retries <= 0
93
-
94
- # a little rescue magic
95
- puts "ERROR: Could not auth, will now exit."
96
- puts "ERROR: Unable to locate cookie, retry in 1 second."
97
- @retries -= 1
98
- sleep 1
99
- data(hexdata)
100
118
  end
101
119
 
102
- # encode plain to hex
103
- def hex_encode(plain)
104
- return nil if plain.nil?
120
+ # Declare client state transitions, for ease of code reading.
121
+ # It is just a pair.
122
+ NextState = Struct.new(:state, :command_words)
105
123
 
106
- plain.to_s.unpack1("H*")
107
- end
124
+ # Authenticates the connection before messages can be exchanged.
125
+ class Client
126
+ # @return [Boolean] have we negotiated Unix file descriptor passing
127
+ # NOTE: not implemented yet in upper layers
128
+ attr_reader :unix_fd
108
129
 
109
- # decode hex to plain
110
- def hex_decode(encoded)
111
- encoded.scan(/[[:xdigit:]]{2}/).map { |h| h.hex.chr }.join
112
- end
113
- end
130
+ # @return [String]
131
+ attr_reader :address_uuid
114
132
 
115
- # Note: this following stuff is tested with External authenticator only!
133
+ # Create a new authentication client.
134
+ # @param mechs [Array<Mechanism,Class>,nil] custom list of auth Mechanism objects or classes
135
+ def initialize(socket, mechs = nil)
136
+ @unix_fd = false
137
+ @address_uuid = nil
116
138
 
117
- # = Authentication client class.
118
- #
119
- # Class tha performs the actional authentication.
120
- class Client
121
- # Create a new authentication client.
122
- def initialize(socket)
123
- @socket = socket
124
- @state = nil
125
- @auth_list = [External, DBusCookieSHA1, Anonymous]
126
- end
139
+ @socket = socket
140
+ @state = nil
141
+ @auth_list = mechs || [
142
+ External,
143
+ DBusCookieSHA1,
144
+ ExternalWithoutUid,
145
+ Anonymous
146
+ ]
147
+ end
148
+
149
+ # Start the authentication process.
150
+ # @return [void]
151
+ # @raise [AuthenticationFailed]
152
+ def authenticate
153
+ DBus.logger.debug "Authenticating"
154
+ send_nul_byte
127
155
 
128
- # Start the authentication process.
129
- def authenticate
130
- if RbConfig::CONFIG["target_os"] =~ /freebsd/
131
- @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
132
- else
133
- @socket.write(0.chr)
156
+ use_next_mechanism
157
+
158
+ @state, command = next_state_via_mechanism.to_a
159
+ send(command)
160
+
161
+ loop do
162
+ DBus.logger.debug "auth STATE: #{@state}"
163
+ words = next_msg
164
+
165
+ @state, command = next_state(words).to_a
166
+ break if [:TerminatedOk, :TerminatedError].include? @state
167
+
168
+ send(command)
169
+ end
170
+
171
+ raise AuthenticationFailed, command.first if @state == :TerminatedError
172
+
173
+ send("BEGIN")
134
174
  end
135
- next_authenticator
136
- @state = :Starting
137
- while @state != :Authenticated
138
- r = next_state
139
- return r if !r
175
+
176
+ ##########
177
+
178
+ private
179
+
180
+ ##########
181
+
182
+ # The authentication protocol requires a nul byte
183
+ # that may carry credentials.
184
+ # @return [void]
185
+ def send_nul_byte
186
+ if RbConfig::CONFIG["target_os"] =~ /freebsd/
187
+ @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
188
+ else
189
+ @socket.write(0.chr)
190
+ end
140
191
  end
141
- true
142
- end
143
192
 
144
- ##########
193
+ # encode plain to hex
194
+ # @param plain [String,nil]
195
+ # @return [String,nil]
196
+ def hex_encode(plain)
197
+ return nil if plain.nil?
145
198
 
146
- private
199
+ plain.unpack1("H*")
200
+ end
147
201
 
148
- ##########
202
+ # decode hex to plain
203
+ # @param encoded [String,nil]
204
+ # @return [String,nil]
205
+ def hex_decode(encoded)
206
+ return nil if encoded.nil?
149
207
 
150
- # Send an authentication method _meth_ with arguments _args_ to the
151
- # server.
152
- def send(meth, *args)
153
- o = ([meth] + args).join(" ")
154
- @socket.write("#{o}\r\n")
155
- end
208
+ [encoded].pack("H*")
209
+ end
156
210
 
157
- # Try authentication using the next authenticator.
158
- def next_authenticator
159
- raise AuthenticationFailed if @auth_list.empty?
160
-
161
- @authenticator = @auth_list.shift.new
162
- auth_msg = ["AUTH", @authenticator.name, @authenticator.authenticate]
163
- DBus.logger.debug "auth_msg: #{auth_msg.inspect}"
164
- send(auth_msg)
165
- rescue AuthenticationFailed
166
- @socket.close
167
- raise
168
- end
211
+ # Send a string to the socket; good place for test mocks.
212
+ def write_line(str)
213
+ DBus.logger.debug "auth_write: #{str.inspect}"
214
+ @socket.write(str)
215
+ end
169
216
 
170
- # Read data (a buffer) from the bus until CR LF is encountered.
171
- # Return the buffer without the CR LF characters.
172
- def next_msg
173
- data = ""
174
- crlf = "\r\n"
175
- left = 1024 # 1024 byte, no idea if it's ever getting bigger
176
- while left.positive?
177
- buf = @socket.read(left > 1 ? 1 : left)
178
- break if buf.nil?
179
-
180
- left -= buf.bytesize
181
- data += buf
182
- break if data.include? crlf # crlf means line finished, the TCP socket keeps on listening, so we break
217
+ # Send *words* to the server as a single CRLF terminated string.
218
+ # @param words [Array<String>,String]
219
+ def send(words)
220
+ joined = Array(words).compact.join(" ")
221
+ write_line("#{joined}\r\n")
183
222
  end
184
- readline = data.chomp.split(" ")
185
- DBus.logger.debug "readline: #{readline.inspect}"
186
- readline
187
- end
188
223
 
189
- # # Read data (a buffer) from the bus until CR LF is encountered.
190
- # # Return the buffer without the CR LF characters.
191
- # def next_msg
192
- # @socket.readline.chomp.split(" ")
193
- # end
194
-
195
- # Try to reach the next state based on the current state.
196
- def next_state
197
- msg = next_msg
198
- if @state == :Starting
199
- DBus.logger.debug ":Starting msg: #{msg[0].inspect}"
200
- case msg[0]
201
- when "OK"
202
- @state = :WaitingForOk
203
- when "CONTINUE"
204
- @state = :WaitingForData
205
- when "REJECTED" # needed by tcp, unix-path/abstract doesn't get here
206
- @state = :WaitingForData
207
- end
224
+ # Try authentication using the next mechanism.
225
+ # @raise [AuthenticationFailed] if there are no more left
226
+ # @return [void]
227
+ def use_next_mechanism
228
+ raise AuthenticationFailed, "Authentication mechanisms exhausted" if @auth_list.empty?
229
+
230
+ @mechanism = @auth_list.shift
231
+ @mechanism = @mechanism.new if @mechanism.is_a? Class
232
+ rescue AuthenticationFailed
233
+ # TODO: make this caller's responsibility
234
+ @socket.close
235
+ raise
208
236
  end
209
- DBus.logger.debug "state: #{@state}"
210
- case @state
211
- when :WaitingForData
212
- DBus.logger.debug ":WaitingForData msg: #{msg[0].inspect}"
213
- case msg[0]
214
- when "DATA"
215
- chall = msg[1]
216
- resp, chall = @authenticator.data(chall)
217
- DBus.logger.debug ":WaitingForData/DATA resp: #{resp.inspect}"
218
- case resp
219
- when :AuthContinue
220
- send("DATA", chall)
221
- @state = :WaitingForData
222
- when :AuthOk
223
- send("DATA", chall)
224
- @state = :WaitingForOk
225
- when :AuthError
226
- send("ERROR")
227
- @state = :WaitingForData
228
- end
229
- when "REJECTED"
230
- next_authenticator
231
- @state = :WaitingForData
232
- when "ERROR"
233
- send("CANCEL")
234
- @state = :WaitingForReject
235
- when "OK"
236
- send("BEGIN")
237
- @state = :Authenticated
238
- else
239
- send("ERROR")
240
- @state = :WaitingForData
237
+
238
+ # Read data (a buffer) from the bus until CR LF is encountered.
239
+ # Return the buffer without the CR LF characters.
240
+ # @return [Array<String>] received words
241
+ def next_msg
242
+ read_line.chomp.split(" ")
243
+ end
244
+
245
+ # Read a line from the socket; good place for test mocks.
246
+ # @return [String] CRLF (\r\n) terminated
247
+ def read_line
248
+ # TODO: probably can simply call @socket.readline
249
+ data = ""
250
+ crlf = "\r\n"
251
+ left = 1024 # 1024 byte, no idea if it's ever getting bigger
252
+ while left.positive?
253
+ buf = @socket.read(left > 1 ? 1 : left)
254
+ break if buf.nil?
255
+
256
+ left -= buf.bytesize
257
+ data += buf
258
+ break if data.include? crlf # crlf means line finished, the TCP socket keeps on listening, so we break
241
259
  end
242
- when :WaitingForOk
243
- DBus.logger.debug ":WaitingForOk msg: #{msg[0].inspect}"
244
- case msg[0]
245
- when "OK"
246
- send("BEGIN")
247
- @state = :Authenticated
248
- when "REJECT"
249
- next_authenticator
250
- @state = :WaitingForData
251
- when "DATA", "ERROR"
252
- send("CANCEL")
253
- @state = :WaitingForReject
260
+ DBus.logger.debug "auth_read: #{data.inspect}"
261
+ data
262
+ end
263
+
264
+ # # Read data (a buffer) from the bus until CR LF is encountered.
265
+ # # Return the buffer without the CR LF characters.
266
+ # def next_msg
267
+ # @socket.readline.chomp.split(" ")
268
+ # end
269
+
270
+ # @param hex_challenge [String,nil] (nil when the server said "DATA\r\n")
271
+ # @param use_data [Boolean] say DATA instead of AUTH
272
+ # @return [NextState]
273
+ def next_state_via_mechanism(hex_challenge = nil, use_data: false)
274
+ challenge = hex_decode(hex_challenge)
275
+
276
+ action, response = @mechanism.call(challenge)
277
+ DBus.logger.debug "auth mechanism action: #{action.inspect}"
278
+
279
+ command = use_data ? ["DATA"] : ["AUTH", @mechanism.name]
280
+
281
+ case action
282
+ when :MechError
283
+ NextState.new(:WaitingForData, ["ERROR", response])
284
+ when :MechContinue
285
+ NextState.new(:WaitingForData, command + [hex_encode(response)])
286
+ when :MechOk
287
+ NextState.new(:WaitingForOk, command + [hex_encode(response)])
254
288
  else
255
- send("ERROR")
256
- @state = :WaitingForOk
289
+ raise AuthenticationFailed, "internal error, unknown action #{action.inspect} " \
290
+ "from our mechanism #{@mechanism.inspect}"
257
291
  end
258
- when :WaitingForReject
259
- DBus.logger.debug ":WaitingForReject msg: #{msg[0].inspect}"
260
- case msg[0]
261
- when "REJECT"
262
- next_authenticator
263
- @state = :WaitingForOk
292
+ end
293
+
294
+ # Try to reach the next state based on the current state.
295
+ # @param received_words [Array<String>]
296
+ # @return [NextState]
297
+ def next_state(received_words)
298
+ msg = received_words
299
+
300
+ case @state
301
+ when :WaitingForData
302
+ case msg[0]
303
+ when "DATA"
304
+ next_state_via_mechanism(msg[1], use_data: true)
305
+ when "REJECTED"
306
+ use_next_mechanism
307
+ next_state_via_mechanism
308
+ when "ERROR"
309
+ NextState.new(:WaitingForReject, ["CANCEL"])
310
+ when "OK"
311
+ @address_uuid = msg[1]
312
+ # NextState.new(:TerminatedOk, [])
313
+ NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
314
+ else
315
+ NextState.new(:WaitingForData, ["ERROR"])
316
+ end
317
+ when :WaitingForOk
318
+ case msg[0]
319
+ when "OK"
320
+ @address_uuid = msg[1]
321
+ # NextState.new(:TerminatedOk, [])
322
+ NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
323
+ when "REJECTED"
324
+ use_next_mechanism
325
+ next_state_via_mechanism
326
+ when "DATA", "ERROR"
327
+ NextState.new(:WaitingForReject, ["CANCEL"])
328
+ else
329
+ # we don't understand server's response but still wait for a successful auth completion
330
+ NextState.new(:WaitingForOk, ["ERROR"])
331
+ end
332
+ when :WaitingForReject
333
+ case msg[0]
334
+ when "REJECTED"
335
+ use_next_mechanism
336
+ next_state_via_mechanism
337
+ else
338
+ # TODO: spec says to close socket, clarify
339
+ NextState.new(:TerminatedError, ["Unknown server reply #{msg[0].inspect} when expecting REJECTED"])
340
+ end
341
+ when :WaitingForAgreeUnixFD
342
+ case msg[0]
343
+ when "AGREE_UNIX_FD"
344
+ @unix_fd = true
345
+ NextState.new(:TerminatedOk, [])
346
+ when "ERROR"
347
+ @unix_fd = false
348
+ NextState.new(:TerminatedOk, [])
349
+ else
350
+ # TODO: spec says to close socket, clarify
351
+ NextState.new(:TerminatedError, ["Unknown server reply #{msg[0].inspect} to NEGOTIATE_UNIX_FD"])
352
+ end
264
353
  else
265
- @socket.close
266
- return false
354
+ raise "Internal error: unhandled state #{@state.inspect}"
267
355
  end
268
356
  end
269
- true
270
357
  end
271
358
  end
272
359
  end
@@ -19,9 +19,11 @@ module DBus
19
19
  attr_reader :socket
20
20
 
21
21
  def initialize(address)
22
+ DBus.logger.debug "MessageQueue: #{address}"
22
23
  @address = address
23
24
  @buffer = ""
24
25
  @is_tcp = false
26
+ @mutex = Mutex.new
25
27
  connect
26
28
  end
27
29
 
@@ -32,23 +34,28 @@ module DBus
32
34
  # @raise EOFError
33
35
  # @todo failure modes
34
36
  def pop(blocking: true)
35
- buffer_from_socket_nonblock
36
- message = message_from_buffer_nonblock
37
- if blocking
38
- # we can block
39
- while message.nil?
40
- r, _d, _d = IO.select([@socket])
41
- if r && r[0] == @socket
42
- buffer_from_socket_nonblock
43
- message = message_from_buffer_nonblock
37
+ # FIXME: this is not enough, the R/W test deadlocks on shared connections
38
+ @mutex.synchronize do
39
+ buffer_from_socket_nonblock
40
+ message = message_from_buffer_nonblock
41
+ if blocking
42
+ # we can block
43
+ while message.nil?
44
+ r, _d, _d = IO.select([@socket])
45
+ if r && r[0] == @socket
46
+ buffer_from_socket_nonblock
47
+ message = message_from_buffer_nonblock
48
+ end
44
49
  end
45
50
  end
51
+ message
46
52
  end
47
- message
48
53
  end
49
54
 
50
55
  def push(message)
51
- @socket.write(message.marshall)
56
+ @mutex.synchronize do
57
+ @socket.write(message.marshall)
58
+ end
52
59
  end
53
60
  alias << push
54
61
 
@@ -129,7 +136,7 @@ module DBus
129
136
 
130
137
  # Initialize the connection to the bus.
131
138
  def init_connection
132
- client = Client.new(@socket)
139
+ client = Authentication::Client.new(@socket)
133
140
  client.authenticate
134
141
  end
135
142
 
data/spec/auth_spec.rb ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env rspec
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "spec_helper"
5
+ require "dbus"
6
+
7
+ describe DBus::Authentication::Client do
8
+ let(:socket) { instance_double("Socket") }
9
+ let(:subject) { described_class.new(socket) }
10
+
11
+ before(:each) do
12
+ allow(Process).to receive(:uid).and_return(999)
13
+ allow(subject).to receive(:send_nul_byte)
14
+ end
15
+
16
+ describe "#next_state" do
17
+ it "raises when I forget to handle a state" do
18
+ subject.instance_variable_set(:@state, :Denmark)
19
+ expect { subject.__send__(:next_state, []) }.to raise_error(RuntimeError, /unhandled state :Denmark/)
20
+ end
21
+ end
22
+
23
+ def expect_protocol(pairs)
24
+ pairs.each do |we_say, server_says|
25
+ expect(subject).to receive(:write_line).with(we_say)
26
+ next if server_says.nil?
27
+
28
+ expect(subject).to receive(:read_line).and_return(server_says)
29
+ end
30
+ end
31
+
32
+ context "with ANONYMOUS" do
33
+ let(:subject) { described_class.new(socket, [DBus::Authentication::Anonymous]) }
34
+
35
+ it "authentication passes" do
36
+ expect_protocol [
37
+ ["AUTH ANONYMOUS 527562792044427573\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
38
+ ["NEGOTIATE_UNIX_FD\r\n", "ERROR not for anonymous\r\n"],
39
+ ["BEGIN\r\n"]
40
+ ]
41
+
42
+ expect { subject.authenticate }.to_not raise_error
43
+ end
44
+ end
45
+
46
+ context "with EXTERNAL" do
47
+ let(:subject) { described_class.new(socket, [DBus::Authentication::External]) }
48
+
49
+ it "authentication passes, and address_uuid is set" do
50
+ expect_protocol [
51
+ ["AUTH EXTERNAL 393939\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
52
+ ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
53
+ ["BEGIN\r\n"]
54
+ ]
55
+
56
+ expect { subject.authenticate }.to_not raise_error
57
+ expect(subject.address_uuid).to eq "ffffffffffffffffffffffffffffffff"
58
+ end
59
+
60
+ context "when the server says superfluous things before an OK" do
61
+ it "authentication passes" do
62
+ expect_protocol [
63
+ ["AUTH EXTERNAL 393939\r\n", "WOULD_YOU_LIKE_SOME_TEA\r\n"],
64
+ ["ERROR\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
65
+ ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
66
+ ["BEGIN\r\n"]
67
+ ]
68
+
69
+ expect { subject.authenticate }.to_not raise_error
70
+ end
71
+ end
72
+
73
+ context "when the server messes up NEGOTIATE_UNIX_FD" do
74
+ it "authentication fails orderly" do
75
+ expect_protocol [
76
+ ["AUTH EXTERNAL 393939\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
77
+ ["NEGOTIATE_UNIX_FD\r\n", "I_DONT_NEGOTIATE_WITH_TENORISTS\r\n"]
78
+ ]
79
+
80
+ allow(socket).to receive(:close) # want to get rid of this
81
+ # TODO: quote the server error message?
82
+ expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /Unknown server reply/)
83
+ end
84
+ end
85
+
86
+ context "when the server replies with ERROR" do
87
+ it "authentication fails orderly" do
88
+ expect_protocol [
89
+ ["AUTH EXTERNAL 393939\r\n", "ERROR something failed\r\n"],
90
+ ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
91
+ ]
92
+
93
+ allow(socket).to receive(:close) # want to get rid of this
94
+ # TODO: quote the server error message?
95
+ expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /exhausted/)
96
+ end
97
+ end
98
+ end
99
+
100
+ context "with EXTERNAL without uid" do
101
+ let(:subject) do
102
+ described_class.new(socket, [DBus::Authentication::External, DBus::Authentication::ExternalWithoutUid])
103
+ end
104
+
105
+ it "authentication passes" do
106
+ expect_protocol [
107
+ ["AUTH EXTERNAL 393939\r\n", "REJECTED EXTERNAL\r\n"],
108
+ # this succeeds when we connect to a privileged container,
109
+ # where outside-non-root becomes inside-root
110
+ ["AUTH EXTERNAL\r\n", "DATA\r\n"],
111
+ ["DATA\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
112
+ ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
113
+ ["BEGIN\r\n"]
114
+ ]
115
+
116
+ expect { subject.authenticate }.to_not raise_error
117
+ end
118
+ end
119
+
120
+ context "with a rejected mechanism and then EXTERNAL" do
121
+ let(:rejected_mechanism) do
122
+ double("Mechanism", name: "WIMP", call: [:MechContinue, "I expect to be rejected"])
123
+ end
124
+
125
+ let(:subject) { described_class.new(socket, [rejected_mechanism, DBus::Authentication::External]) }
126
+
127
+ it "authentication eventually passes" do
128
+ expect_protocol [
129
+ [/^AUTH WIMP .*\r\n/, "REJECTED EXTERNAL\r\n"],
130
+ ["AUTH EXTERNAL 393939\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
131
+ ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
132
+ ["BEGIN\r\n"]
133
+ ]
134
+
135
+ expect { subject.authenticate }.to_not raise_error
136
+ end
137
+ end
138
+
139
+ context "with a DATA-using mechanism" do
140
+ let(:mechanism) do
141
+ double("Mechanism", name: "CHALLENGE_ME", call: [:MechContinue, "1"])
142
+ end
143
+
144
+ # try it twice to test calling #use_next_mechanism
145
+ let(:subject) { described_class.new(socket, [mechanism, mechanism]) }
146
+
147
+ it "authentication fails orderly when the server says ERROR" do
148
+ expect_protocol [
149
+ ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
150
+ ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"],
151
+ ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
152
+ ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
153
+ ]
154
+
155
+ allow(socket).to receive(:close) # want to get rid of this
156
+ # TODO: quote the server error message?
157
+ expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /exhausted/)
158
+ end
159
+
160
+ it "authentication fails orderly when the server says ERROR and then changes its mind" do
161
+ expect_protocol [
162
+ ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
163
+ ["CANCEL\r\n", "I_CHANGED_MY_MIND please come back\r\n"]
164
+ ]
165
+
166
+ allow(socket).to receive(:close) # want to get rid of this
167
+ # TODO: quote the server error message?
168
+ expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /Unknown.*MIND.*REJECTED/)
169
+ end
170
+
171
+ it "authentication passes when the server says superfluous things before DATA" do
172
+ expect_protocol [
173
+ ["AUTH CHALLENGE_ME 31\r\n", "WOULD_YOU_LIKE_SOME_TEA\r\n"],
174
+ ["ERROR\r\n", "DATA\r\n"],
175
+ ["DATA 31\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
176
+ ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
177
+ ["BEGIN\r\n"]
178
+ ]
179
+
180
+ expect { subject.authenticate }.to_not raise_error
181
+ end
182
+
183
+ it "authentication passes when the server decides not to need the DATA" do
184
+ expect_protocol [
185
+ ["AUTH CHALLENGE_ME 31\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
186
+ ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
187
+ ["BEGIN\r\n"]
188
+ ]
189
+
190
+ expect { subject.authenticate }.to_not raise_error
191
+ end
192
+ end
193
+
194
+ context "with a mechanism returning :MechError" do
195
+ let(:fallible_mechanism) do
196
+ double(name: "FALLIBLE", call: [:MechError, "not my best day"])
197
+ end
198
+
199
+ let(:subject) { described_class.new(socket, [fallible_mechanism]) }
200
+
201
+ it "authentication fails orderly" do
202
+ expect_protocol [
203
+ ["ERROR not my best day\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
204
+ ]
205
+
206
+ allow(socket).to receive(:close) # want to get rid of thise
207
+ expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /exhausted/)
208
+ end
209
+ end
210
+
211
+ context "with a badly implemented mechanism" do
212
+ let(:buggy_mechanism) do
213
+ double(name: "buggy", call: [:smurf, nil])
214
+ end
215
+
216
+ let(:subject) { described_class.new(socket, [buggy_mechanism]) }
217
+
218
+ it "authentication fails before protoxol is exchanged" do
219
+ expect(subject).to_not receive(:write_line)
220
+ expect(subject).to_not receive(:read_line)
221
+
222
+ expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /smurf/)
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env rspec
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "spec_helper"
5
+ require "dbus"
6
+
7
+ describe DBus::ProxyObjectInterface do
8
+ # TODO: tag tests that need a service, eg "needs-service"
9
+ # TODO: maybe remove this and rely on a packaged tool
10
+ around(:each) do |example|
11
+ with_private_bus do
12
+ with_service_by_activation(&example)
13
+ end
14
+ end
15
+
16
+ let(:bus) { DBus::ASessionBus.new }
17
+
18
+ context "when calling org.ruby.service" do
19
+ let(:svc) { bus["org.ruby.service"] }
20
+
21
+ # This is white box testing, knowing the implementation
22
+ # A better way would be structuring it according to the D-Bus Spec
23
+ # Or testing the service side doing the right thing? (What if our bugs cancel out)
24
+ describe "#define_method_from_descriptor" do
25
+ it "can call a method with multiple OUT arguments" do
26
+ obj = svc["/org/ruby/MyInstance"]
27
+ ifc = obj["org.ruby.SampleInterface"]
28
+
29
+ even, odd = ifc.EvenOdd([3, 1, 4, 1, 5, 9, 2, 6])
30
+ expect(even).to eq [4, 2, 6]
31
+ expect(odd).to eq [3, 1, 1, 5, 9]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -102,6 +102,12 @@ class Test < DBus::Object
102
102
  [coords]
103
103
  end
104
104
 
105
+ # Two OUT arguments
106
+ dbus_method :EvenOdd, "in numbers:ai, out even:ai, out odd:ai" do |numbers|
107
+ even, odd = numbers.partition(&:even?)
108
+ [even, odd]
109
+ end
110
+
105
111
  # Properties:
106
112
  # ReadMe:string, returns "READ ME" at first, then what WriteMe received
107
113
  # WriteMe:string
data/spec/spec_helper.rb CHANGED
@@ -31,7 +31,7 @@ if coverage
31
31
  c.single_report_path = "coverage/lcov.info"
32
32
  end
33
33
 
34
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
34
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [
35
35
  SimpleCov::Formatter::HTMLFormatter,
36
36
  SimpleCov::Formatter::LcovFormatter
37
37
  ]
@@ -5,28 +5,71 @@
5
5
  require_relative "spec_helper"
6
6
  require "dbus"
7
7
 
8
- describe "ThreadSafetyTest" do
9
- it "tests thread competition" do
10
- print "Thread competition: "
11
- jobs = []
12
- 5.times do
13
- jobs << Thread.new do
14
- Thread.current.abort_on_exception = true
8
+ class TestSignalRace < DBus::Object
9
+ dbus_interface "org.ruby.ServerTest" do
10
+ dbus_signal :signal_without_arguments
11
+ end
12
+ end
13
+
14
+ # Run *count* threads all doing *body*, wait for their finish
15
+ def race_threads(count, &body)
16
+ jobs = count.times.map do |j|
17
+ Thread.new do
18
+ Thread.current.abort_on_exception = true
19
+
20
+ body.call(j)
21
+ end
22
+ end
23
+ jobs.each(&:join)
24
+ end
25
+
26
+ # Repeat *count* times: { random sleep, *body* }, printing progress
27
+ def repeat_with_jitter(count, &body)
28
+ count.times do |i|
29
+ sleep 0.1 * rand
30
+ print "#{i} "
31
+ $stdout.flush
15
32
 
33
+ body.call
34
+ end
35
+ end
36
+
37
+ describe "thread safety" do
38
+ context "R/W: when the threads call methods with return values" do
39
+ it "it works with separate bus connections" do
40
+ race_threads(5) do |_j|
16
41
  # use separate connections to avoid races
17
42
  bus = DBus::ASessionBus.new
18
43
  svc = bus.service("org.ruby.service")
19
44
  obj = svc.object("/org/ruby/MyInstance")
20
45
  obj.default_iface = "org.ruby.SampleInterface"
21
46
 
22
- 10.times do |i|
23
- print "#{i} "
24
- $stdout.flush
47
+ repeat_with_jitter(10) do
25
48
  expect(obj.the_answer[0]).to eq(42)
26
- sleep 0.1 * rand
27
49
  end
28
50
  end
51
+ puts
52
+ end
53
+ end
54
+
55
+ context "W/O: when the threads only send signals" do
56
+ it "it works with a shared separate bus connection" do
57
+ race_threads(5) do |j|
58
+ # shared connection
59
+ bus = DBus::SessionBus.instance
60
+ # hackish: we do not actually request the name
61
+ svc = DBus::Service.new("org.ruby.server-test#{j}", bus)
62
+
63
+ obj = TestSignalRace.new "/org/ruby/Foo"
64
+ svc.export obj
65
+
66
+ repeat_with_jitter(10) do
67
+ obj.signal_without_arguments
68
+ end
69
+
70
+ svc.unexport(obj)
71
+ end
72
+ puts
29
73
  end
30
- jobs.each(&:join)
31
74
  end
32
75
  end
@@ -7,6 +7,20 @@
7
7
  <!-- Our well-known bus type, don't change this -->
8
8
  <type>session</type>
9
9
 
10
+ <!-- Authentication:
11
+ This was useful during refactoring, but meanwhile RSpec mocking has
12
+ replaced it. -->
13
+ <!-- Explicitly list all known authentication mechanisms,
14
+ their order is not important.
15
+ By default the daemon allows all but this lets me disable some. -->
16
+ <auth>EXTERNAL</auth>
17
+ <auth>DBUS_COOKIE_SHA1</auth>
18
+ <auth>ANONYMOUS</auth>
19
+ <!-- Insecure, other users could call us and exploit debug APIs/bugs -->
20
+ <!--
21
+ <allow_anonymous/>
22
+ -->
23
+
10
24
  <listen>unix:tmpdir=/tmp</listen>
11
25
  <listen>tcp:host=127.0.0.1</listen>
12
26
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-dbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruby DBus Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-18 00:00:00.000000000 Z
11
+ date: 2023-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rexml
@@ -165,6 +165,7 @@ files:
165
165
  - lib/dbus/xml.rb
166
166
  - ruby-dbus.gemspec
167
167
  - spec/async_spec.rb
168
+ - spec/auth_spec.rb
168
169
  - spec/binding_spec.rb
169
170
  - spec/bus_and_xml_backend_spec.rb
170
171
  - spec/bus_driver_spec.rb
@@ -186,6 +187,7 @@ files:
186
187
  - spec/packet_marshaller_spec.rb
187
188
  - spec/packet_unmarshaller_spec.rb
188
189
  - spec/property_spec.rb
190
+ - spec/proxy_object_interface_spec.rb
189
191
  - spec/proxy_object_spec.rb
190
192
  - spec/server_robustness_spec.rb
191
193
  - spec/server_spec.rb
@@ -223,8 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
223
225
  - !ruby/object:Gem::Version
224
226
  version: '0'
225
227
  requirements: []
226
- rubyforge_project:
227
- rubygems_version: 2.7.6.3
228
+ rubygems_version: 3.3.0.dev
228
229
  signing_key:
229
230
  specification_version: 4
230
231
  summary: Ruby module for interaction with D-Bus