1 | /* |
2 | * Copyright 2019 Collabora Ltd. |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General |
15 | * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | #include "config.h" |
19 | |
20 | #include <errno.h> |
21 | |
22 | #include <glib/gstdio.h> |
23 | #include <gio/gio.h> |
24 | |
25 | /* For G_CREDENTIALS_*_SUPPORTED */ |
26 | #include <gio/gcredentialsprivate.h> |
27 | |
28 | #ifdef HAVE_DBUS1 |
29 | #include <dbus/dbus.h> |
30 | #endif |
31 | |
32 | typedef enum |
33 | { |
34 | INTEROP_FLAGS_EXTERNAL = (1 << 0), |
35 | INTEROP_FLAGS_ANONYMOUS = (1 << 1), |
36 | INTEROP_FLAGS_SHA1 = (1 << 2), |
37 | INTEROP_FLAGS_TCP = (1 << 3), |
38 | INTEROP_FLAGS_LIBDBUS = (1 << 4), |
39 | INTEROP_FLAGS_ABSTRACT = (1 << 5), |
40 | INTEROP_FLAGS_REQUIRE_SAME_USER = (1 << 6), |
41 | INTEROP_FLAGS_NONE = 0 |
42 | } InteropFlags; |
43 | |
44 | static gboolean |
45 | allow_external_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, |
46 | const char *mechanism, |
47 | G_GNUC_UNUSED gpointer user_data) |
48 | { |
49 | if (g_strcmp0 (str1: mechanism, str2: "EXTERNAL" ) == 0) |
50 | { |
51 | g_debug ("Accepting EXTERNAL authentication" ); |
52 | return TRUE; |
53 | } |
54 | else |
55 | { |
56 | g_debug ("Rejecting \"%s\" authentication: not EXTERNAL" , mechanism); |
57 | return FALSE; |
58 | } |
59 | } |
60 | |
61 | static gboolean |
62 | allow_anonymous_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, |
63 | const char *mechanism, |
64 | G_GNUC_UNUSED gpointer user_data) |
65 | { |
66 | if (g_strcmp0 (str1: mechanism, str2: "ANONYMOUS" ) == 0) |
67 | { |
68 | g_debug ("Accepting ANONYMOUS authentication" ); |
69 | return TRUE; |
70 | } |
71 | else |
72 | { |
73 | g_debug ("Rejecting \"%s\" authentication: not ANONYMOUS" , mechanism); |
74 | return FALSE; |
75 | } |
76 | } |
77 | |
78 | static gboolean |
79 | allow_sha1_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, |
80 | const char *mechanism, |
81 | G_GNUC_UNUSED gpointer user_data) |
82 | { |
83 | if (g_strcmp0 (str1: mechanism, str2: "DBUS_COOKIE_SHA1" ) == 0) |
84 | { |
85 | g_debug ("Accepting DBUS_COOKIE_SHA1 authentication" ); |
86 | return TRUE; |
87 | } |
88 | else |
89 | { |
90 | g_debug ("Rejecting \"%s\" authentication: not DBUS_COOKIE_SHA1" , |
91 | mechanism); |
92 | return FALSE; |
93 | } |
94 | } |
95 | |
96 | static gboolean |
97 | allow_any_mechanism_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, |
98 | const char *mechanism, |
99 | G_GNUC_UNUSED gpointer user_data) |
100 | { |
101 | g_debug ("Accepting \"%s\" authentication" , mechanism); |
102 | return TRUE; |
103 | } |
104 | |
105 | static gboolean |
106 | authorize_any_authenticated_peer_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, |
107 | G_GNUC_UNUSED GIOStream *stream, |
108 | GCredentials *credentials, |
109 | G_GNUC_UNUSED gpointer user_data) |
110 | { |
111 | if (credentials == NULL) |
112 | { |
113 | g_debug ("Authorizing peer with no credentials" ); |
114 | } |
115 | else |
116 | { |
117 | gchar *str = g_credentials_to_string (credentials); |
118 | |
119 | g_debug ("Authorizing peer with credentials: %s" , str); |
120 | g_free (mem: str); |
121 | } |
122 | |
123 | return TRUE; |
124 | } |
125 | |
126 | static GDBusMessage * |
127 | whoami_filter_cb (GDBusConnection *connection, |
128 | GDBusMessage *message, |
129 | gboolean incoming, |
130 | G_GNUC_UNUSED gpointer user_data) |
131 | { |
132 | if (!incoming) |
133 | return message; |
134 | |
135 | if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_METHOD_CALL && |
136 | g_strcmp0 (str1: g_dbus_message_get_member (message), str2: "WhoAmI" ) == 0) |
137 | { |
138 | GDBusMessage *reply = g_dbus_message_new_method_reply (method_call_message: message); |
139 | gint64 uid = -1; |
140 | gint64 pid = -1; |
141 | #ifdef G_OS_UNIX |
142 | GCredentials *credentials = g_dbus_connection_get_peer_credentials (connection); |
143 | |
144 | if (credentials != NULL) |
145 | { |
146 | uid = (gint64) g_credentials_get_unix_user (credentials, NULL); |
147 | pid = (gint64) g_credentials_get_unix_pid (credentials, NULL); |
148 | } |
149 | #endif |
150 | |
151 | g_dbus_message_set_body (message: reply, |
152 | body: g_variant_new (format_string: "(xx)" , uid, pid)); |
153 | g_dbus_connection_send_message (connection, message: reply, |
154 | flags: G_DBUS_SEND_MESSAGE_FLAGS_NONE, |
155 | NULL, NULL); |
156 | g_object_unref (object: reply); |
157 | |
158 | /* handled */ |
159 | g_object_unref (object: message); |
160 | return NULL; |
161 | } |
162 | |
163 | return message; |
164 | } |
165 | |
166 | static gboolean |
167 | new_connection_cb (G_GNUC_UNUSED GDBusServer *server, |
168 | GDBusConnection *connection, |
169 | G_GNUC_UNUSED gpointer user_data) |
170 | { |
171 | GCredentials *credentials = g_dbus_connection_get_peer_credentials (connection); |
172 | |
173 | if (credentials == NULL) |
174 | { |
175 | g_debug ("New connection from peer with no credentials" ); |
176 | } |
177 | else |
178 | { |
179 | gchar *str = g_credentials_to_string (credentials); |
180 | |
181 | g_debug ("New connection from peer with credentials: %s" , str); |
182 | g_free (mem: str); |
183 | } |
184 | |
185 | g_object_ref (connection); |
186 | g_dbus_connection_add_filter (connection, filter_function: whoami_filter_cb, NULL, NULL); |
187 | return TRUE; |
188 | } |
189 | |
190 | #ifdef HAVE_DBUS1 |
191 | typedef struct |
192 | { |
193 | DBusError error; |
194 | DBusConnection *conn; |
195 | DBusMessage *call; |
196 | DBusMessage *reply; |
197 | } LibdbusCall; |
198 | |
199 | static void |
200 | libdbus_call_task_cb (GTask *task, |
201 | G_GNUC_UNUSED gpointer source_object, |
202 | gpointer task_data, |
203 | G_GNUC_UNUSED GCancellable *cancellable) |
204 | { |
205 | LibdbusCall *libdbus_call = task_data; |
206 | |
207 | libdbus_call->reply = dbus_connection_send_with_reply_and_block (libdbus_call->conn, |
208 | libdbus_call->call, |
209 | -1, |
210 | &libdbus_call->error); |
211 | } |
212 | #endif /* HAVE_DBUS1 */ |
213 | |
214 | static void |
215 | store_result_cb (G_GNUC_UNUSED GObject *source_object, |
216 | GAsyncResult *res, |
217 | gpointer user_data) |
218 | { |
219 | GAsyncResult **result = user_data; |
220 | |
221 | g_assert_nonnull (result); |
222 | g_assert_null (*result); |
223 | *result = g_object_ref (res); |
224 | } |
225 | |
226 | static void |
227 | assert_expected_uid_pid (InteropFlags flags, |
228 | gint64 uid, |
229 | gint64 pid) |
230 | { |
231 | #ifdef G_OS_UNIX |
232 | if (flags & (INTEROP_FLAGS_ANONYMOUS | INTEROP_FLAGS_SHA1 | INTEROP_FLAGS_TCP)) |
233 | { |
234 | /* No assertion. There is no guarantee whether credentials will be |
235 | * passed even though we didn't send them. Conversely, if |
236 | * credentials were not passed, |
237 | * g_dbus_connection_get_peer_credentials() always returns the |
238 | * credentials of the socket, and not the uid that a |
239 | * client might have proved it has by using DBUS_COOKIE_SHA1. */ |
240 | } |
241 | else /* We should prefer EXTERNAL whenever it is allowed. */ |
242 | { |
243 | #ifdef __linux__ |
244 | /* We know that both GDBus and libdbus support full credentials-passing |
245 | * on Linux. */ |
246 | g_assert_cmpint (uid, ==, getuid ()); |
247 | g_assert_cmpint (pid, ==, getpid ()); |
248 | #elif defined(__APPLE__) |
249 | /* We know (or at least suspect) that both GDBus and libdbus support |
250 | * passing the uid only on macOS. */ |
251 | g_assert_cmpint (uid, ==, getuid ()); |
252 | /* No pid here */ |
253 | #else |
254 | g_test_message ("Please open a merge request to add appropriate " |
255 | "assertions for your platform" ); |
256 | #endif |
257 | } |
258 | #endif /* G_OS_UNIX */ |
259 | } |
260 | |
261 | static void |
262 | do_test_server_auth (InteropFlags flags) |
263 | { |
264 | GError *error = NULL; |
265 | gchar *tmpdir = NULL; |
266 | gchar *listenable_address = NULL; |
267 | GDBusServer *server = NULL; |
268 | GDBusAuthObserver *observer = NULL; |
269 | GDBusServerFlags server_flags = G_DBUS_SERVER_FLAGS_RUN_IN_THREAD; |
270 | gchar *guid = NULL; |
271 | const char *connectable_address; |
272 | GDBusConnection *client = NULL; |
273 | GAsyncResult *result = NULL; |
274 | GVariant *tuple = NULL; |
275 | gint64 uid, pid; |
276 | #ifdef HAVE_DBUS1 |
277 | /* GNOME/glib#1831 seems to involve a race condition, so try a few times |
278 | * to see if we can trigger it. */ |
279 | gsize i; |
280 | gsize n = 20; |
281 | #endif |
282 | |
283 | if (flags & INTEROP_FLAGS_TCP) |
284 | { |
285 | listenable_address = g_strdup (str: "tcp:host=127.0.0.1" ); |
286 | } |
287 | else |
288 | { |
289 | #ifdef G_OS_UNIX |
290 | gchar *escaped; |
291 | |
292 | tmpdir = g_dir_make_tmp (tmpl: "gdbus-server-auth-XXXXXX" , error: &error); |
293 | g_assert_no_error (error); |
294 | escaped = g_dbus_address_escape_value (string: tmpdir); |
295 | listenable_address = g_strdup_printf (format: "unix:%s=%s" , |
296 | (flags & INTEROP_FLAGS_ABSTRACT) ? "tmpdir" : "dir" , |
297 | escaped); |
298 | g_free (mem: escaped); |
299 | #else |
300 | g_test_skip ("unix: addresses only work on Unix" ); |
301 | goto out; |
302 | #endif |
303 | } |
304 | |
305 | g_test_message (format: "Testing GDBus server at %s / libdbus client, with flags: " |
306 | "external:%s " |
307 | "anonymous:%s " |
308 | "sha1:%s " |
309 | "abstract:%s " |
310 | "tcp:%s" , |
311 | listenable_address, |
312 | (flags & INTEROP_FLAGS_EXTERNAL) ? "true" : "false" , |
313 | (flags & INTEROP_FLAGS_ANONYMOUS) ? "true" : "false" , |
314 | (flags & INTEROP_FLAGS_SHA1) ? "true" : "false" , |
315 | (flags & INTEROP_FLAGS_ABSTRACT) ? "true" : "false" , |
316 | (flags & INTEROP_FLAGS_TCP) ? "true" : "false" ); |
317 | |
318 | #if !defined(G_CREDENTIALS_UNIX_CREDENTIALS_MESSAGE_SUPPORTED) \ |
319 | && !defined(G_CREDENTIALS_SOCKET_GET_CREDENTIALS_SUPPORTED) |
320 | if (flags & INTEROP_FLAGS_EXTERNAL) |
321 | { |
322 | g_test_skip ("EXTERNAL authentication not implemented on this platform" ); |
323 | goto out; |
324 | } |
325 | #endif |
326 | |
327 | if (flags & INTEROP_FLAGS_ANONYMOUS) |
328 | server_flags |= G_DBUS_SERVER_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS; |
329 | if (flags & INTEROP_FLAGS_REQUIRE_SAME_USER) |
330 | server_flags |= G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER; |
331 | |
332 | observer = g_dbus_auth_observer_new (); |
333 | |
334 | if (flags & INTEROP_FLAGS_EXTERNAL) |
335 | g_signal_connect (observer, "allow-mechanism" , |
336 | G_CALLBACK (allow_external_cb), NULL); |
337 | else if (flags & INTEROP_FLAGS_ANONYMOUS) |
338 | g_signal_connect (observer, "allow-mechanism" , |
339 | G_CALLBACK (allow_anonymous_cb), NULL); |
340 | else if (flags & INTEROP_FLAGS_SHA1) |
341 | g_signal_connect (observer, "allow-mechanism" , |
342 | G_CALLBACK (allow_sha1_cb), NULL); |
343 | else |
344 | g_signal_connect (observer, "allow-mechanism" , |
345 | G_CALLBACK (allow_any_mechanism_cb), NULL); |
346 | |
347 | g_signal_connect (observer, "authorize-authenticated-peer" , |
348 | G_CALLBACK (authorize_any_authenticated_peer_cb), |
349 | NULL); |
350 | |
351 | guid = g_dbus_generate_guid (); |
352 | server = g_dbus_server_new_sync (address: listenable_address, |
353 | flags: server_flags, |
354 | guid, |
355 | observer, |
356 | NULL, |
357 | error: &error); |
358 | g_assert_no_error (error); |
359 | g_assert_nonnull (server); |
360 | g_signal_connect (server, "new-connection" , G_CALLBACK (new_connection_cb), NULL); |
361 | g_dbus_server_start (server); |
362 | connectable_address = g_dbus_server_get_client_address (server); |
363 | g_test_message (format: "Connectable address: %s" , connectable_address); |
364 | |
365 | result = NULL; |
366 | g_dbus_connection_new_for_address (address: connectable_address, |
367 | flags: G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, |
368 | NULL, NULL, callback: store_result_cb, user_data: &result); |
369 | |
370 | while (result == NULL) |
371 | g_main_context_iteration (NULL, TRUE); |
372 | |
373 | client = g_dbus_connection_new_for_address_finish (res: result, error: &error); |
374 | g_assert_no_error (error); |
375 | g_assert_nonnull (client); |
376 | g_clear_object (&result); |
377 | |
378 | g_dbus_connection_call (connection: client, NULL, object_path: "/" , interface_name: "com.example.Test" , method_name: "WhoAmI" , |
379 | NULL, G_VARIANT_TYPE ("(xx)" ), |
380 | flags: G_DBUS_CALL_FLAGS_NONE, timeout_msec: -1, NULL, callback: store_result_cb, |
381 | user_data: &result); |
382 | |
383 | while (result == NULL) |
384 | g_main_context_iteration (NULL, TRUE); |
385 | |
386 | tuple = g_dbus_connection_call_finish (connection: client, res: result, error: &error); |
387 | g_assert_no_error (error); |
388 | g_assert_nonnull (tuple); |
389 | g_clear_object (&result); |
390 | g_clear_object (&client); |
391 | |
392 | uid = -2; |
393 | pid = -2; |
394 | g_variant_get (value: tuple, format_string: "(xx)" , &uid, &pid); |
395 | |
396 | g_debug ("Server says GDBus client is uid %" G_GINT64_FORMAT ", pid %" G_GINT64_FORMAT, |
397 | uid, pid); |
398 | |
399 | assert_expected_uid_pid (flags, uid, pid); |
400 | |
401 | g_clear_pointer (&tuple, g_variant_unref); |
402 | |
403 | #ifdef HAVE_DBUS1 |
404 | for (i = 0; i < n; i++) |
405 | { |
406 | LibdbusCall libdbus_call = { DBUS_ERROR_INIT, NULL, NULL, NULL }; |
407 | GTask *task; |
408 | |
409 | /* The test suite uses %G_TEST_OPTION_ISOLATE_DIRS, which sets |
410 | * `HOME=/dev/null` and leaves g_get_home_dir() pointing to the per-test |
411 | * temp home directory. Unfortunately, libdbus doesn’t allow the home dir |
412 | * to be overridden except using the environment, so copy the per-test |
413 | * temp home directory back there so that libdbus uses the same |
414 | * `$HOME/.dbus-keyrings` path as GLib. This is not thread-safe. */ |
415 | g_setenv ("HOME" , g_get_home_dir (), TRUE); |
416 | |
417 | libdbus_call.conn = dbus_connection_open_private (connectable_address, |
418 | &libdbus_call.error); |
419 | g_assert_cmpstr (libdbus_call.error.name, ==, NULL); |
420 | g_assert_nonnull (libdbus_call.conn); |
421 | |
422 | libdbus_call.call = dbus_message_new_method_call (NULL, "/" , |
423 | "com.example.Test" , |
424 | "WhoAmI" ); |
425 | |
426 | if (libdbus_call.call == NULL) |
427 | g_error ("Out of memory" ); |
428 | |
429 | result = NULL; |
430 | task = g_task_new (NULL, NULL, store_result_cb, &result); |
431 | g_task_set_task_data (task, &libdbus_call, NULL); |
432 | g_task_run_in_thread (task, libdbus_call_task_cb); |
433 | |
434 | while (result == NULL) |
435 | g_main_context_iteration (NULL, TRUE); |
436 | |
437 | g_clear_object (&result); |
438 | |
439 | g_assert_cmpstr (libdbus_call.error.name, ==, NULL); |
440 | g_assert_nonnull (libdbus_call.reply); |
441 | |
442 | uid = -2; |
443 | pid = -2; |
444 | dbus_message_get_args (libdbus_call.reply, &libdbus_call.error, |
445 | DBUS_TYPE_INT64, &uid, |
446 | DBUS_TYPE_INT64, &pid, |
447 | DBUS_TYPE_INVALID); |
448 | g_assert_cmpstr (libdbus_call.error.name, ==, NULL); |
449 | |
450 | g_debug ("Server says libdbus client %" G_GSIZE_FORMAT " is uid %" G_GINT64_FORMAT ", pid %" G_GINT64_FORMAT, |
451 | i, uid, pid); |
452 | assert_expected_uid_pid (flags | INTEROP_FLAGS_LIBDBUS, uid, pid); |
453 | |
454 | dbus_connection_close (libdbus_call.conn); |
455 | dbus_connection_unref (libdbus_call.conn); |
456 | dbus_message_unref (libdbus_call.call); |
457 | dbus_message_unref (libdbus_call.reply); |
458 | g_clear_object (&task); |
459 | } |
460 | #else /* !HAVE_DBUS1 */ |
461 | g_test_skip (msg: "Testing interop with libdbus not supported" ); |
462 | #endif /* !HAVE_DBUS1 */ |
463 | |
464 | /* No practical effect, just to avoid -Wunused-label under some |
465 | * combinations of #ifdefs */ |
466 | goto out; |
467 | |
468 | out: |
469 | if (server != NULL) |
470 | g_dbus_server_stop (server); |
471 | |
472 | if (tmpdir != NULL) |
473 | g_assert_cmpstr (g_rmdir (tmpdir) == 0 ? "OK" : g_strerror (errno), |
474 | ==, "OK" ); |
475 | |
476 | g_clear_object (&server); |
477 | g_clear_object (&observer); |
478 | g_free (mem: guid); |
479 | g_free (mem: listenable_address); |
480 | g_free (mem: tmpdir); |
481 | } |
482 | |
483 | static void |
484 | test_server_auth (void) |
485 | { |
486 | do_test_server_auth (flags: INTEROP_FLAGS_NONE); |
487 | } |
488 | |
489 | static void |
490 | test_server_auth_abstract (void) |
491 | { |
492 | do_test_server_auth (flags: INTEROP_FLAGS_ABSTRACT); |
493 | } |
494 | |
495 | static void |
496 | test_server_auth_tcp (void) |
497 | { |
498 | do_test_server_auth (flags: INTEROP_FLAGS_TCP); |
499 | } |
500 | |
501 | static void |
502 | test_server_auth_anonymous (void) |
503 | { |
504 | do_test_server_auth (flags: INTEROP_FLAGS_ANONYMOUS); |
505 | } |
506 | |
507 | static void |
508 | test_server_auth_anonymous_tcp (void) |
509 | { |
510 | do_test_server_auth (flags: INTEROP_FLAGS_ANONYMOUS | INTEROP_FLAGS_TCP); |
511 | } |
512 | |
513 | static void |
514 | test_server_auth_external (void) |
515 | { |
516 | do_test_server_auth (flags: INTEROP_FLAGS_EXTERNAL); |
517 | } |
518 | |
519 | static void |
520 | test_server_auth_external_require_same_user (void) |
521 | { |
522 | do_test_server_auth (flags: INTEROP_FLAGS_EXTERNAL | INTEROP_FLAGS_REQUIRE_SAME_USER); |
523 | } |
524 | |
525 | static void |
526 | test_server_auth_sha1 (void) |
527 | { |
528 | do_test_server_auth (flags: INTEROP_FLAGS_SHA1); |
529 | } |
530 | |
531 | static void |
532 | test_server_auth_sha1_tcp (void) |
533 | { |
534 | do_test_server_auth (flags: INTEROP_FLAGS_SHA1 | INTEROP_FLAGS_TCP); |
535 | } |
536 | |
537 | int |
538 | main (int argc, |
539 | char *argv[]) |
540 | { |
541 | g_test_init (argc: &argc, argv: &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); |
542 | |
543 | g_test_add_func (testpath: "/gdbus/server-auth" , test_func: test_server_auth); |
544 | g_test_add_func (testpath: "/gdbus/server-auth/abstract" , test_func: test_server_auth_abstract); |
545 | g_test_add_func (testpath: "/gdbus/server-auth/tcp" , test_func: test_server_auth_tcp); |
546 | g_test_add_func (testpath: "/gdbus/server-auth/anonymous" , test_func: test_server_auth_anonymous); |
547 | g_test_add_func (testpath: "/gdbus/server-auth/anonymous/tcp" , test_func: test_server_auth_anonymous_tcp); |
548 | g_test_add_func (testpath: "/gdbus/server-auth/external" , test_func: test_server_auth_external); |
549 | g_test_add_func (testpath: "/gdbus/server-auth/external/require-same-user" , test_func: test_server_auth_external_require_same_user); |
550 | g_test_add_func (testpath: "/gdbus/server-auth/sha1" , test_func: test_server_auth_sha1); |
551 | g_test_add_func (testpath: "/gdbus/server-auth/sha1/tcp" , test_func: test_server_auth_sha1_tcp); |
552 | |
553 | return g_test_run(); |
554 | } |
555 | |