123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- 'use strict';
- const Gio = imports.gi.Gio;
- const GIRepository = imports.gi.GIRepository;
- const GLib = imports.gi.GLib;
- const Config = imports.config;
- const {setup, setupGettext} = imports.utils.setup;
- // Promise Wrappers
- try {
- const {EBook, EDataServer} = imports.gi;
- Gio._promisify(EBook.BookClient, 'connect');
- Gio._promisify(EBook.BookClient.prototype, 'get_view');
- Gio._promisify(EBook.BookClient.prototype, 'get_contacts');
- Gio._promisify(EDataServer.SourceRegistry, 'new');
- } catch (e) {
- // Silence import errors
- }
- Gio._promisify(Gio.AsyncInitable.prototype, 'init_async');
- Gio._promisify(Gio.DBusConnection.prototype, 'call');
- Gio._promisify(Gio.DBusProxy.prototype, 'call');
- Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async',
- 'read_line_finish_utf8');
- Gio._promisify(Gio.File.prototype, 'delete_async');
- Gio._promisify(Gio.File.prototype, 'enumerate_children_async');
- Gio._promisify(Gio.File.prototype, 'load_contents_async');
- Gio._promisify(Gio.File.prototype, 'mount_enclosing_volume');
- Gio._promisify(Gio.File.prototype, 'query_info_async');
- Gio._promisify(Gio.File.prototype, 'read_async');
- Gio._promisify(Gio.File.prototype, 'replace_async');
- Gio._promisify(Gio.File.prototype, 'replace_contents_bytes_async',
- 'replace_contents_finish');
- Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async');
- Gio._promisify(Gio.Mount.prototype, 'unmount_with_operation');
- Gio._promisify(Gio.InputStream.prototype, 'close_async');
- Gio._promisify(Gio.OutputStream.prototype, 'close_async');
- Gio._promisify(Gio.OutputStream.prototype, 'splice_async');
- Gio._promisify(Gio.OutputStream.prototype, 'write_all_async');
- Gio._promisify(Gio.SocketClient.prototype, 'connect_async');
- Gio._promisify(Gio.SocketListener.prototype, 'accept_async');
- Gio._promisify(Gio.Subprocess.prototype, 'communicate_utf8_async');
- Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');
- Gio._promisify(Gio.TlsConnection.prototype, 'handshake_async');
- Gio._promisify(Gio.DtlsConnection.prototype, 'handshake_async');
- // User Directories
- Config.CACHEDIR = GLib.build_filenamev([GLib.get_user_cache_dir(), 'gsconnect']);
- Config.CONFIGDIR = GLib.build_filenamev([GLib.get_user_config_dir(), 'gsconnect']);
- Config.RUNTIMEDIR = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gsconnect']);
- // Bootstrap
- setup(Config.PACKAGE_DATADIR);
- setupGettext();
- if (Config.IS_USER) {
- // Infer libdir by assuming gnome-shell shares a common prefix with gjs;
- // assume the parent directory if it's not there
- let libdir = GIRepository.Repository.get_search_path().find(path => {
- return path.endsWith('/gjs/girepository-1.0');
- }).replace('/gjs/girepository-1.0', '');
- const gsdir = GLib.build_filenamev([libdir, 'gnome-shell']);
- if (!GLib.file_test(gsdir, GLib.FileTest.IS_DIR)) {
- const currentDir = `/${GLib.path_get_basename(libdir)}`;
- libdir = libdir.replace(currentDir, '');
- }
- Config.GNOME_SHELL_LIBDIR = libdir;
- }
- // Load DBus interfaces
- Config.DBUS = (() => {
- const bytes = Gio.resources_lookup_data(
- GLib.build_filenamev([Config.APP_PATH, `${Config.APP_ID}.xml`]),
- Gio.ResourceLookupFlags.NONE
- );
- const xml = new TextDecoder().decode(bytes.toArray());
- const dbus = Gio.DBusNodeInfo.new_for_xml(xml);
- dbus.nodes.forEach(info => info.cache_build());
- return dbus;
- })();
- // Init User Directories
- for (const path of [Config.CACHEDIR, Config.CONFIGDIR, Config.RUNTIMEDIR])
- GLib.mkdir_with_parents(path, 0o755);
- /**
- * Check if we're in a Wayland session (mostly for input synthesis)
- * https://wiki.gnome.org/Accessibility/Wayland#Bugs.2FIssues_We_Must_Address
- */
- globalThis.HAVE_REMOTEINPUT = GLib.getenv('GDMSESSION') !== 'ubuntu-wayland';
- globalThis.HAVE_WAYLAND = GLib.getenv('XDG_SESSION_TYPE') === 'wayland';
- globalThis.HAVE_GNOME = GLib.getenv('GNOME_SETUP_DISPLAY') !== null;
- /**
- * A custom debug function that logs at LEVEL_MESSAGE to avoid the need for env
- * variables to be set.
- *
- * @param {Error|string} message - A string or Error to log
- * @param {string} [prefix] - An optional prefix for the warning
- */
- const _debugCallerMatch = new RegExp(/([^@]*)@([^:]*):([^:]*)/);
- // eslint-disable-next-line func-style
- const _debugFunc = function (error, prefix = null) {
- let caller, message;
- if (error.stack) {
- caller = error.stack.split('\n')[0];
- message = `${error.message}\n${error.stack}`;
- } else {
- caller = (new Error()).stack.split('\n')[1];
- message = JSON.stringify(error, null, 2);
- }
- if (prefix)
- message = `${prefix}: ${message}`;
- const [, func, file, line] = _debugCallerMatch.exec(caller);
- const script = file.replace(Config.PACKAGE_DATADIR, '');
- GLib.log_structured('GSConnect', GLib.LogLevelFlags.LEVEL_MESSAGE, {
- 'MESSAGE': `[${script}:${func}:${line}]: ${message}`,
- 'SYSLOG_IDENTIFIER': 'org.gnome.Shell.Extensions.GSConnect',
- 'CODE_FILE': file,
- 'CODE_FUNC': func,
- 'CODE_LINE': line,
- });
- };
- // Swap the function out for a no-op anonymous function for speed
- const settings = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true),
- });
- settings.connect('changed::debug', (settings, key) => {
- globalThis.debug = settings.get_boolean(key) ? _debugFunc : () => {};
- });
- if (settings.get_boolean('debug'))
- globalThis.debug = _debugFunc;
- else
- globalThis.debug = () => {};
- /**
- * Start wl_clipboard if not under Gnome
- */
- if (!globalThis.HAVE_GNOME) {
- debug('Not running as a Gnome extension');
- imports.wl_clipboard.watchService();
- }
- /**
- * A simple (for now) pre-comparison sanitizer for phone numbers
- * See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210
- *
- * @return {string} Return the string stripped of leading 0, and ' ()-+'
- */
- String.prototype.toPhoneNumber = function () {
- const strippedNumber = this.replace(/^0*|[ ()+-]/g, '');
- if (strippedNumber.length)
- return strippedNumber;
- return this;
- };
- /**
- * A simple equality check for phone numbers based on `toPhoneNumber()`
- *
- * @param {string} number - A phone number string to compare
- * @return {boolean} If `this` and @number are equivalent phone numbers
- */
- String.prototype.equalsPhoneNumber = function (number) {
- const a = this.toPhoneNumber();
- const b = number.toPhoneNumber();
- return (a.length && b.length && (a.endsWith(b) || b.endsWith(a)));
- };
- /**
- * An implementation of `rm -rf` in Gio
- *
- * @param {Gio.File|string} file - a GFile or filepath
- */
- Gio.File.rm_rf = function (file) {
- try {
- if (typeof file === 'string')
- file = Gio.File.new_for_path(file);
- try {
- const iter = file.enumerate_children(
- 'standard::name',
- Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
- null
- );
- let info;
- while ((info = iter.next_file(null)))
- Gio.File.rm_rf(iter.get_child(info));
- iter.close(null);
- } catch (e) {
- // Silence errors
- }
- file.delete(null);
- } catch (e) {
- // Silence errors
- }
- };
- /**
- * Extend GLib.Variant with a static method to recursively pack a variant
- *
- * @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
- * @return {GLib.Variant} The resulting GVariant
- */
- function _full_pack(obj) {
- let packed;
- const type = typeof obj;
- switch (true) {
- case (obj instanceof GLib.Variant):
- return obj;
- case (type === 'string'):
- return GLib.Variant.new('s', obj);
- case (type === 'number'):
- return GLib.Variant.new('d', obj);
- case (type === 'boolean'):
- return GLib.Variant.new('b', obj);
- case (obj instanceof Uint8Array):
- return GLib.Variant.new('ay', obj);
- case (obj === null):
- return GLib.Variant.new('mv', null);
- case (typeof obj.map === 'function'):
- return GLib.Variant.new(
- 'av',
- obj.filter(e => e !== undefined).map(e => _full_pack(e))
- );
- case (obj instanceof Gio.Icon):
- return obj.serialize();
- case (type === 'object'):
- packed = {};
- for (const [key, val] of Object.entries(obj)) {
- if (val !== undefined)
- packed[key] = _full_pack(val);
- }
- return GLib.Variant.new('a{sv}', packed);
- default:
- throw Error(`Unsupported type '${type}': ${obj}`);
- }
- }
- GLib.Variant.full_pack = _full_pack;
- /**
- * Extend GLib.Variant with a method to recursively deepUnpack() a variant
- *
- * @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
- * @return {*} The resulting object
- */
- function _full_unpack(obj) {
- obj = (obj === undefined) ? this : obj;
- const unpacked = {};
- switch (true) {
- case (obj === null):
- return obj;
- case (obj instanceof GLib.Variant):
- return _full_unpack(obj.deepUnpack());
- case (obj instanceof Uint8Array):
- return obj;
- case (typeof obj.map === 'function'):
- return obj.map(e => _full_unpack(e));
- case (typeof obj === 'object'):
- for (const [key, value] of Object.entries(obj)) {
- // Try to detect and deserialize GIcons
- try {
- if (key === 'icon' && value.get_type_string() === '(sv)')
- unpacked[key] = Gio.Icon.deserialize(value);
- else
- unpacked[key] = _full_unpack(value);
- } catch (e) {
- unpacked[key] = _full_unpack(value);
- }
- }
- return unpacked;
- default:
- return obj;
- }
- }
- GLib.Variant.prototype.full_unpack = _full_unpack;
- /**
- * Creates a GTlsCertificate from the PEM-encoded data in @cert_path and
- * @key_path. If either are missing a new pair will be generated.
- *
- * Additionally, the private key will be added using ssh-add to allow sftp
- * connections using Gio.
- *
- * See: https://github.com/KDE/kdeconnect-kde/blob/master/core/kdeconnectconfig.cpp#L119
- *
- * @param {string} certPath - Absolute path to a x509 certificate in PEM format
- * @param {string} keyPath - Absolute path to a private key in PEM format
- * @param {string} commonName - A unique common name for the certificate
- * @return {Gio.TlsCertificate} A TLS certificate
- */
- Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) {
- // Check if the certificate/key pair already exists
- const certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS);
- const keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS);
- // Create a new certificate and private key if necessary
- if (!certExists || !keyExists) {
- // If we weren't passed a common name, generate a random one
- if (!commonName)
- commonName = GLib.uuid_string_random();
- const proc = new Gio.Subprocess({
- argv: [
- Config.OPENSSL_PATH, 'req',
- '-new', '-x509', '-sha256',
- '-out', certPath,
- '-newkey', 'rsa:4096', '-nodes',
- '-keyout', keyPath,
- '-days', '3650',
- '-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`,
- ],
- flags: (Gio.SubprocessFlags.STDOUT_SILENCE |
- Gio.SubprocessFlags.STDERR_SILENCE),
- });
- proc.init(null);
- proc.wait_check(null);
- }
- return Gio.TlsCertificate.new_from_files(certPath, keyPath);
- };
- Object.defineProperties(Gio.TlsCertificate.prototype, {
- /**
- * The common name of the certificate.
- */
- 'common_name': {
- get: function () {
- if (!this.__common_name) {
- const proc = new Gio.Subprocess({
- argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-subject', '-inform', 'pem'],
- flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE,
- });
- proc.init(null);
- const stdout = proc.communicate_utf8(this.certificate_pem, null)[1];
- this.__common_name = /(?:cn|CN) ?= ?([^,\n]*)/.exec(stdout)[1];
- }
- return this.__common_name;
- },
- configurable: true,
- enumerable: true,
- },
- /**
- * Get just the pubkey as a DER ByteArray of a certificate.
- *
- * @return {GLib.Bytes} The pubkey as DER of the certificate.
- */
- 'pubkey_der': {
- value: function () {
- if (!this.__pubkey_der) {
- let proc = new Gio.Subprocess({
- argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-pubkey', '-inform', 'pem'],
- flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE,
- });
- proc.init(null);
- const pubkey = proc.communicate_utf8(this.certificate_pem, null)[1];
- proc = new Gio.Subprocess({
- argv: [Config.OPENSSL_PATH, 'pkey', '-pubin', '-inform', 'pem', '-outform', 'der'],
- flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE,
- });
- proc.init(null);
- this.__pubkey_der = proc.communicate(new TextEncoder().encode(pubkey), null)[1];
- }
- return this.__pubkey_der;
- },
- configurable: true,
- enumerable: false,
- },
- });
|