diff --git a/data/meson.build b/data/meson.build index d1511623..ec2b0d88 100644 --- a/data/meson.build +++ b/data/meson.build @@ -3,6 +3,13 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: Michael Terry +google_client_id_parts = get_option('google_client_id').split('.') +google_client_id_parts_reversed = [] +foreach part : google_client_id_parts + google_client_id_parts_reversed = [part] + google_client_id_parts_reversed +endforeach +google_reversedns = '.'.join(google_client_id_parts_reversed) + conf_data = configuration_data() conf_data.set('appid', application_id) conf_data.set('bindir', bindir) @@ -11,6 +18,7 @@ conf_data.set('gsettingspath', profile == '' ? 'deja-dup' : 'deja-dup-' + profil conf_data.set('icon', application_id) conf_data.set('pkglibexecdir', pkglibexecdir) conf_data.set('profile', profile) +conf_data.set('scheme_google', google_reversedns) conf_data.set('version', meson.project_version()) install_data(join_paths('icons', '@0@.svg'.format(application_id)), diff --git a/data/org.gnome.DejaDup.desktop.in b/data/org.gnome.DejaDup.desktop.in index afa30e4a..5784ccef 100644 --- a/data/org.gnome.DejaDup.desktop.in +++ b/data/org.gnome.DejaDup.desktop.in @@ -7,7 +7,7 @@ Comment=Change your backup settings Icon=@icon@ -Exec=deja-dup +Exec=deja-dup %u StartupNotify=true DBusActivatable=true @@ -15,6 +15,9 @@ DBusActivatable=true Type=Application Categories=Utility;Archiving;GNOME;GTK;X-GNOME-Utilities; +# Used for oauth flows (server redirects to this custom scheme and we catch it) +MimeType=x-scheme-handler/@scheme_google@; + # Translators: Add whatever keywords you want in your language, separated by semicolons # These keywords are used when searching for applications in dashes, etc. Keywords=déjà;deja;dup; diff --git a/data/post-install.sh b/data/post-install.sh index 2de522d8..7648584b 100644 --- a/data/post-install.sh +++ b/data/post-install.sh @@ -12,4 +12,7 @@ if [ -z "$DESTDIR" ]; then echo "Updating gsettings cache..." glib-compile-schemas "$datadir/glib-2.0/schemas" + + echo "Updating desktop mime cache..." + update-desktop-database -q "$datadir/applications" fi diff --git a/deja-dup/main.vala b/deja-dup/main.vala index 7613b9db..e62fb730 100644 --- a/deja-dup/main.vala +++ b/deja-dup/main.vala @@ -53,8 +53,14 @@ public class DejaDupApp : Gtk.Application private DejaDupApp() { - Object(application_id: Config.APPLICATION_ID, - flags: ApplicationFlags.HANDLES_COMMAND_LINE); + Object( + application_id: Config.APPLICATION_ID, + flags: ApplicationFlags.HANDLES_COMMAND_LINE | + // HANDLES_OPEN is required to support Open calls over dbus, which + // we use for our registered custom schemes (which support our + // oauth2 workflow). + ApplicationFlags.HANDLES_OPEN + ); add_main_option_entries(OPTIONS); } @@ -71,14 +77,15 @@ public class DejaDupApp : Gtk.Application { var options = command_line.get_options_dict(); - string[] filenames = {}; + File[] files = {}; if (options.contains("")) { var variant = options.lookup_value("", VariantType.BYTESTRING_ARRAY); - filenames = variant.get_bytestring_array(); + foreach (var filename in variant.get_bytestring_array()) + files += command_line.create_file_for_arg(filename); } if (options.contains("restore")) { - if (filenames.length == 0) { + if (files.length == 0) { command_line.printerr("%s\n", _("Please list files to restore")); return 1; } @@ -90,9 +97,8 @@ public class DejaDupApp : Gtk.Application } var file_list = new List<File>(); - int i = 0; - while (filenames[i] != null) - file_list.append(command_line.create_file_for_arg(filenames[i++])); + foreach (var file in files) + file_list.append(file); restore_files(file_list); } @@ -112,6 +118,14 @@ public class DejaDupApp : Gtk.Application } else if (options.contains("prompt")) { Notifications.prompt(); + } + else if (files.length > 0) { + // If we were called without a mode (like --restore) but with file arguments, + // let's do our "Open" action (which is mostly used for our oauth flow). + // That oauth flow can happen via command line in some environments like + // snaps, whereas the dbus Open call might happen for flatpaks. Regardless + // of how they come in, treat them the same. + open(files, ""); } else { activate(); } @@ -143,6 +157,28 @@ public class DejaDupApp : Gtk.Application } } + public override void open(GLib.File[] files, string hint) + { + var google_backend = get_restore_backend() as DejaDup.BackendGoogle; + + // We might be in middle of oauth flow, and are given an expected redirect uri + // like 'com.googleusercontent.apps.123:/oauth2redirect?code=xxx' + if (files.length == 1 && google_backend != null) + { + var provided_uri = files[0].get_uri(); + // Normalize backend URI through gio, so it matches incoming URI format (slashes after colon, etc) + var expected_uri = File.new_for_uri(google_backend.get_redirect_uri()).get_uri(); + if (provided_uri.has_prefix(expected_uri) && google_backend.continue_authorization(provided_uri)) { + activate(); + return; + } + } + + // Got passed files, but we don't know what to do with them. + foreach (var file in files) + warning("Ignoring unexpected file: %s", file.get_parse_name()); + } + void show() { activate(); diff --git a/libdeja/BackendGoogle.vala b/libdeja/BackendGoogle.vala index 3b294b45..5dbd6091 100644 --- a/libdeja/BackendGoogle.vala +++ b/libdeja/BackendGoogle.vala @@ -23,9 +23,7 @@ public const string GOOGLE_SERVER = "google.com"; public class BackendGoogle : Backend { - Soup.Server server; Soup.Session session; - string local_address; string pkce; string credentials_dir; string access_token; @@ -232,7 +230,7 @@ public class BackendGoogle : Backend "GET", "https://accounts.google.com/o/oauth2/v2/auth", "client_id", Config.GOOGLE_CLIENT_ID, - "redirect_uri", local_address, + "redirect_uri", get_redirect_uri(), "response_type", "code", "code_challenge", pkce, "scope", "https://www.googleapis.com/auth/drive.file" @@ -246,7 +244,7 @@ public class BackendGoogle : Backend "POST", "https://www.googleapis.com/oauth2/v4/token", "client_id", Config.GOOGLE_CLIENT_ID, - "redirect_uri", local_address, + "redirect_uri", get_redirect_uri(), "grant_type", "authorization_code", "code_verifier", pkce, "code", code @@ -266,49 +264,12 @@ public class BackendGoogle : Backend yield get_tokens(message); } - void oauth_server_request_received(Soup.Server server, Soup.Message message, - string path, - HashTable<string, string>? query, - Soup.ClientContext client) - { - if (path != "/") { - message.status_code = Soup.Status.NOT_FOUND; - return; - } - - message.status_code = Soup.Status.ACCEPTED; - server = null; - - string? error = query == null ? null : query.lookup("error"); - if (error != null) { - stop_login(error); - return; - } - - string? code = query == null ? null : query.lookup("code"); - if (code == null) { - stop_login(null); - return; - } - - // Show consent granted screen - var html = DejaDup.get_access_granted_html(); - message.response_body.append_take(html.data); - - show_oauth_consent_page(null, null); // continue on from paused screen - get_credentials.begin(code); - } - void start_authorization() throws Error { - // Start a server and listen on it - server = new Soup.Server("server-header", - "%s/%s ".printf(Config.PACKAGE, Config.VERSION)); - server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY); - local_address = server.get_uris().data.to_string(false); - // Prepare to handle requests that finish the consent process - server.add_handler(null, oauth_server_request_received); + error_msg = null; + code = null; + authorization_instance.set(this); // We need a random string between 43 and 128 chars. UUIDs are an easy way // to get random strings, but they are only 37 chars long. So just add two. @@ -378,6 +339,59 @@ public class BackendGoogle : Backend envp.append("GOOGLE_DRIVE_SETTINGS=%s/settings.yaml".printf(credentials_dir)); envp_ready(true, envp); } + + static WeakRef authorization_instance; + string error_msg; + string code; + + public string get_redirect_uri() + { + var id_parts = Config.GOOGLE_CLIENT_ID.split("."); + string[] reversed = {}; + for (int i = id_parts.length - 1; i >= 0; i--) { + reversed += id_parts[i]; + } + // Exact path does not matter and is optional. But it seems wise to use some + // path and this one seems reasonable (and is the example in their oauth docs). + return "%s:/oauth2redirect".printf(string.joinv(".", reversed)); + } + + // Meant to be called externally when the oauth service redirects back to us. + // This passed-in uri will hold the code and error values from the service. + public bool continue_authorization(string command_line_redirect_uri) + { + if (authorization_instance.get() == null) + return false; // no auth in progress + + var active_backend = (BackendGoogle)authorization_instance.get(); + active_backend.continue_authorization_helper(command_line_redirect_uri); + return true; + } + + void continue_authorization_helper(string command_line_redirect_uri) + { + HashTable<string,string> query = null; + + try { + var uri = Uri.parse(command_line_redirect_uri, UriFlags.NONE); + query = Uri.parse_params(uri.get_query()); + } catch (UriError e) { + error_msg = e.message; + } + + if (error_msg == null && query != null) + error_msg = query.lookup("error"); + + if (error_msg == null && query != null) + code = query.lookup("code"); + + if (error_msg == null && code == null) + error_msg = ""; // default to just a blank contextual error message + + authorization_instance.set(null); + show_oauth_consent_page(null, null); // continue on from paused screen + get_credentials.begin(code); + } } } // end namespace