From 51618fb8824ab68e7bf476e81042b3c9af976271 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 27 Aug 2024 13:14:15 +0300 Subject: [PATCH 1/7] fixed a possible unhandled exception --- client/secure_qsettings.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index 1e2a2273..88c0242b 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -174,13 +174,25 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json) QByteArray SecureQSettings::encryptText(const QByteArray &value) const { QSimpleCrypto::QBlockCipher cipher; - return cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); + QByteArray result; + try { + result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); + } catch (...) { // todo change error handling in QSimpleCrypto? + qCritical() << "error when encrypting the settings value"; + } + return result; } QByteArray SecureQSettings::decryptText(const QByteArray &ba) const { QSimpleCrypto::QBlockCipher cipher; - return cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); + QByteArray result; + try { + result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); + } catch (...) { // todo change error handling in QSimpleCrypto? + qCritical() << "error when decrypting the settings value"; + } + return result; } bool SecureQSettings::encryptionRequired() const From cd70b7e6190be93bb628941976d22e5788b32445 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Sep 2024 15:54:47 +0300 Subject: [PATCH 2/7] Translation updated (ukrainian) (#1048) * Update amneziavpn_uk_UA.ts --- client/translations/amneziavpn_uk_UA.ts | 502 +++++++++++++----------- 1 file changed, 269 insertions(+), 233 deletions(-) diff --git a/client/translations/amneziavpn_uk_UA.ts b/client/translations/amneziavpn_uk_UA.ts index c7206586..11377c4d 100644 --- a/client/translations/amneziavpn_uk_UA.ts +++ b/client/translations/amneziavpn_uk_UA.ts @@ -29,47 +29,47 @@ Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + Звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео. Працює для будь-яких сайтів. Швидкість до %1 MBit/s VPN to access blocked sites in regions with high levels of Internet censorship. - + VPN для доступу до заблокованих сайтів у регіонах з високим рівнем інтернет-цензури. Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Premium - звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео у високій роздільній здатності. Працює для всіх вебсайтів, навіть у країнах з найвищим рівнем інтернет-цензури. Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + Amnezia Free — це безкоштовний VPN для обходу блокувань у країнах з високим рівнем інтернет-цензури %1 MBit/s - + %1 MBit/s %1 days - + %1 днів VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Лише популярні сайти, які заблоковані у вашому регіоні, будуть відкриватись за допомогою VPN підключення (Instagram, Facebook, Twitter та ін.). Звичайні сайти будуть відкриватися без використання VPN, <a href="%1/free" style="color: #FBB26A;">більш детально на нашому сайті.</a> Free - + Безкоштовно %1 $/month - + %1 $/місяць @@ -77,22 +77,22 @@ Application added: %1 - + Застосунок додано: %1 The application has already been added - + Застосунок вже додано The selected applications have been added - + Вибрані застосунки додані Application removed: %1 - + Застосунок видалено: %1 @@ -100,7 +100,7 @@ Unable to disconnect during configuration preparation - + Неможливо відключитися під час підготовки конфігурації @@ -115,7 +115,7 @@ unable to create configuration - + Неможливо створити конфігурацію @@ -130,7 +130,7 @@ Preparing... - + Підготовка... @@ -140,12 +140,12 @@ Settings updated successfully - Налаштування оновлено. + Налаштування оновлено The selected protocol is not supported on the current platform - Вибраний протокол не підтримується на цьому пристрої + Вибраний протокол не підтримується на цьому пристрої @@ -253,7 +253,7 @@ Enabled Can't be disabled for current server Увімкнено. -Не може бути вимкнено для даного сервера. +Не може бути вимкнено для даного сервера @@ -283,13 +283,13 @@ Can't be disabled for current server Unable to open file - + Неможливо відкрити файл Invalid configuration file - + Недійсний файл конфігурації @@ -299,7 +299,7 @@ Can't be disabled for current server In the imported configuration, potentially dangerous lines were found: - + У імпортованій конфігурації знайдено потенційно небезпечні рядки: @@ -355,12 +355,12 @@ Already installed containers were found on the server. All installed containers Api config removed - + Конфігурацію API видалено %1 cached profile cleared - + Кешований профіль %1 очищено @@ -375,17 +375,17 @@ Already installed containers were found on the server. All installed containers %1 installed successfully. - + %1 встановлено успішно. API config reloaded - + Конфігурацію API перезавантажено Successfully changed the country of connection to %1 - + Успішно змінено країну підключення на %1 @@ -393,17 +393,17 @@ Already installed containers were found on the server. All installed containers Choose application - + Виберіть застосунок application name - + назва застосунку - + Add selected - + Додати вибране @@ -479,17 +479,17 @@ Already installed containers were found on the server. All installed containers Logging enabled - + Логування увімкнено Split tunneling enabled - Роздільне тунелювання увімкнено + Роздільне тунелювання увімкнено Split tunneling disabled - Роздільне тунелювання вимкнено + Роздільне тунелювання вимкнено @@ -572,32 +572,32 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти The values of the H1-H4 fields must be unique - + Значення полів H1-H4 мають бути унікальними The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + Значення поля S1 + розмір повідомлення ініціалізації (148) не має бути рівним значенню S2 + розмір повідомлення відповіді (92) Save settings? - + Зберегти налаштування? All users with whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Remove AmneziaWG @@ -656,12 +656,12 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Save and Restart Amnezia @@ -682,7 +682,7 @@ Already installed containers were found on the server. All installed containers VPN address subnet - + VPN address subnet @@ -840,12 +840,12 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Remove OpenVPN @@ -906,7 +906,7 @@ Already installed containers were found on the server. All installed containers All users with whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. All users with whom you shared a connection will no longer be able to connect to it. @@ -948,12 +948,12 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Save and Restart Amnezia @@ -965,27 +965,27 @@ Already installed containers were found on the server. All installed containers WG settings - + Port - Порт + Порт MTU - + MTU Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення @@ -993,22 +993,22 @@ Already installed containers were found on the server. All installed containers XRay settings - + Налаштування XRay Disguised as traffic from - Замаскувати трафік під + Замаскувати трафік під - + Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення @@ -1030,7 +1030,7 @@ Already installed containers were found on the server. All installed containers The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. - Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку + Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку. @@ -1055,7 +1055,7 @@ Already installed containers were found on the server. All installed containers Cannot remove AmneziaDNS from running server - + Не вдається видалити AmneziaDNS з працюючого сервера @@ -1095,7 +1095,7 @@ Already installed containers were found on the server. All installed containers User name - Імя користувача + Імя користувача @@ -1110,7 +1110,7 @@ Already installed containers were found on the server. All installed containers In order to mount remote SFTP folder as local drive, perform following steps: <br> - Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> + Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> @@ -1151,18 +1151,18 @@ Already installed containers were found on the server. All installed containers Settings updated successfully - + Налаштування успішно оновлено SOCKS5 settings - + Налаштування SOCKS5 Host - Хост + Хост @@ -1170,50 +1170,50 @@ Already installed containers were found on the server. All installed containers Copied - Скопійовано + Скопійовано Port - Порт + Порт User name - + User name Password - Пароль + Пароль Username - + Username Change connection settings - + Змінити налаштування підключення The port must be in the range of 1 to 65535 - + Порт повинен бути в межах від 1 до 65535 Password cannot be empty - + Пароль не може бути порожнім Username cannot be empty - + Ім'я користувача не може бути порожнім @@ -1241,12 +1241,12 @@ Already installed containers were found on the server. All installed containers Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. + Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. + Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. @@ -1309,7 +1309,7 @@ Already installed containers were found on the server. All installed containers Dev console - + @@ -1351,7 +1351,7 @@ Already installed containers were found on the server. All installed containers Amnezia is a free and open-source application. You can support the developers if you like it. - + Amnezia — це безкоштовний додаток з відкритим кодом. Якщо вам подобається цей додаток, ви можете підтримати розробників. @@ -1415,7 +1415,7 @@ Already installed containers were found on the server. All installed containers Privacy Policy - + Політика конфіденційності @@ -1423,74 +1423,74 @@ Already installed containers were found on the server. All installed containers For the region - + Для регіону Price - + Ціна Work period - + Період роботи Speed - + Швидкість Support tag - + Copied - Скопійовано + Скопійовано Reload API config - + Перезавантажити конфігурацію API Reload API config? - + Перезавантажити конфігурацію API? Continue - Продовжити + Продовжити Cancel - Відмінити + Відмінити Cannot reload API config during active connection - + Неможливо перезавантажити конфігурацію API під час активного підключення Remove from application - + Видалити з додатку Remove from application? - + Видалити з додатку? Cannot remove server during active connection - + Неможливо видалити сервер під час активного підключення @@ -1498,57 +1498,57 @@ Already installed containers were found on the server. All installed containers Cannot change split tunneling settings during active connection - Не можна змінити налаштування роздільного тунелювання при підключеному VPN + Не можна змінити налаштування роздільного тунелювання при підключеному VPN Only the apps from the list should have access via VPN - + Доступ через VPN мають лише програми зі списку Apps from the list should not have access via VPN - + Програми зі списку не мають доступ через VPN App split tunneling - + Split tunneling для додатка Mode - Режим + Режим Remove - Видалити + Видалити Continue - Продовжити + Продовжити Cancel - Відмінити + Відмінити application name - + назва додатка Open executable file - + Відкрити виконуваний файл Executable files (*.*) - + Виконувані файли (*.*) @@ -1561,37 +1561,37 @@ Already installed containers were found on the server. All installed containers Allow application screenshots - Дозволити скріншоти в застосунку + Дозволити скріншоти у застосунку Enable notifications - + Увімкнути сповіщення Enable notifications to show the VPN state in the status bar - + Увімкнути сповіщення (показує стан VPN у статус барі) Auto start - Автозапуск + Автозапуск Launch the application every time the device is starts - Запускати застосунок при старті + Запускати застосунок при старті Auto connect - Автопідключення + Автопідключення Connect to VPN on app start - Підключення до VPN при старті застосунку + Підключення до VPN при старті застосунку @@ -1604,7 +1604,7 @@ Already installed containers were found on the server. All installed containers Запускати застосунок в згорнутому вигляді - + Language Мова @@ -1651,7 +1651,7 @@ Already installed containers were found on the server. All installed containers Cannot reset settings during active connection - + Неможливо скинути налаштування під час активного підключення @@ -1672,17 +1672,17 @@ Already installed containers were found on the server. All installed containers Back up your configuration - + Резервне копіювання вашої конфігурації You can save your settings to a backup file to restore them the next time you install the application. - Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми + Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми. The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. - + Резервна копія міститиме ваші паролі та приватні ключі для всіх серверів, доданих до AmneziaVPN. Зберігайте цю інформацію у безпечному місці. @@ -1738,7 +1738,7 @@ Already installed containers were found on the server. All installed containers Cannot restore backup settings during active connection - + Неможливо відновити резервну копію налаштувань під час активного підключення @@ -1774,27 +1774,27 @@ Already installed containers were found on the server. All installed containers When AmneziaDNS is not used or installed - Ці адреси будуть використовуватись коли вимкнений AmneziaDNS + Ці адреси будуть використовуватись коли вимкнений AmneziaDNS Allows you to use the VPN only for certain Apps - Дозволяє використовувати VPN тільки для вибраних застосунків + Дозволяє використовувати VPN тільки для вибраних застосунків KillSwitch - + KillSwitch Disables your internet if your encrypted VPN connection drops out for any reason. - + Вимикає ваш інтернет, якщо ваше захищене VPN-підключення зникає з будь-якої причини. Cannot change killSwitch settings during active connection - + Неможливо змінити налаштування killSwitch під час активного підключення @@ -1821,7 +1821,7 @@ Already installed containers were found on the server. All installed containers Default server does not support custom DNS - Сервер за замовчуванням не підтримує користувацький DNS + Сервер за замовчуванням не підтримує користувацький DNS @@ -1835,7 +1835,7 @@ Already installed containers were found on the server. All installed containers If AmneziaDNS is not used or installed - Якщо AmneziaDNS вимкнено або не встановлено + Якщо AmneziaDNS вимкнено або не встановлено @@ -1888,7 +1888,7 @@ Already installed containers were found on the server. All installed containers Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + Логування увімкнене. Зверніть увагу, що логування буде автоматично вимкнене через 14 днів, а всі файли журналів будуть видалені. @@ -1898,7 +1898,7 @@ Already installed containers were found on the server. All installed containers Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. - + Увімкнення цієї функції автоматично зберігатиме журнали додатка. За замовчуванням функція логування вимкнена. Увімкніть збереження журналів у випадку збою додатка. @@ -2014,62 +2014,62 @@ Already installed containers were found on the server. All installed containers Reboot server - Перезавантажити сервер + Перезавантажити сервер Do you want to reboot the server? - Ви впевнені, що хочете перезавантажити сервер? + Ви впевнені, що хочете перезавантажити сервер? The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? - Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? + Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? Cannot reboot server during active connection - + Неможливо перезавантажити сервер під час активного підключення Remove server from application - + Видалити сервер з додатка Do you want to remove the server from application? - Ви впевнені, що хочете видалити сервер із застосунку? + Ви впевнені, що хочете видалити сервер із застосунку? Cannot remove server during active connection - + Неможливо видалити сервер під час активного підключення Clear server from Amnezia software - + Очистити сервер від програмного забезпечення Amnezia Do you want to clear server from Amnezia software? - + Ви дійсно хочете очистити сервер від програмного забезпечення Amnezia? All users whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. Cannot clear server from Amnezia software during active connection - + Неможливо очистити сервер від програмного забезпечення Amnezia під час активного підключення Cannot reset API config during active connection - + Неможливо скинути конфігурацію API під час активного підключення Do you want to clear server Amnezia-installed services? @@ -2083,7 +2083,7 @@ Already installed containers were found on the server. All installed containers Do you want to reset API config? - Ви хочете скинути API конфігурацію + Ви хочете скинути API конфігурацію? Remove this server from the app @@ -2136,7 +2136,7 @@ Already installed containers were found on the server. All installed containers Management - + Управління Data @@ -2153,22 +2153,22 @@ Already installed containers were found on the server. All installed containers Clear %1 profile - + Очистити профіль %1 Clear %1 profile? - + Очистити профіль %1? Unable to clear %1 profile while there is an active connection - + Неможливо очистити профіль %1 під час активного підключення Cannot remove active container - + Неможливо видалити активний контейнер @@ -2268,17 +2268,17 @@ Already installed containers were found on the server. All installed containers Cannot change split tunneling settings during active connection - Не можна змінити налаштування роздільного тунелювання при підключеному VPN + Не можна змінити налаштування роздільного тунелювання при підключеному VPN Default server does not support split tunneling function - + website or IP - вебсайт або IP + вебсайт або IP @@ -2329,32 +2329,32 @@ Already installed containers were found on the server. All installed containers For the region - + Для регіону Price - + Ціна Work period - + Період роботи Speed - + Швидкість Features - + Особливості Connect - Підключитись + Підключитись @@ -2362,12 +2362,12 @@ Already installed containers were found on the server. All installed containers VPN by Amnezia - + VPN від Amnezia Choose a VPN service that suits your needs. - + Виберіть VPN-сервіс, який відповідає вашим потребам. @@ -2404,67 +2404,67 @@ It's okay as long as it's from someone you trust. Connection - + Підключення Insert the key, add a configuration file or scan the QR-code - + Вставте ключ, додайте файл конфігурації або відскануйте QR-код Insert key - + Вставити ключ Insert - Вставити + Вставити Continue - Продовжити + Продовжити Other connection options - + Інші параметри підключення VPN by Amnezia - + VPN від Amnezia Connect to classic paid and free VPN services from Amnezia - + Підключайтеся до звичайних платних та безкоштовних VPN-сервісів від Amnezia Self-hosted VPN - + Self-hosted VPN Configure Amnezia VPN on your own server - + Налаштуйте Amnezia VPN на власному сервері Restore from backup - Відновити із бекапа + Відновити із бекапа Open backup file - Відкрити бекап файл + Відкрити бекап файл Backup files (*.backup) - Файли резервної копії (*.backup) + Файли резервної копії (*.backup) @@ -2479,7 +2479,7 @@ It's okay as long as it's from someone you trust. I have nothing - У мене нічого нема + У мене нічого нема Key as text @@ -2533,32 +2533,32 @@ and will not be shared or disclosed to the Amnezia or any third parties 255.255.255.255:22 - + 255.255.255.255:22 SSH Username - + SSH Username Password or SSH private key - + Пароль або SSH ключ All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам + Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам How to run your VPN server - + Як запустити ваш VPN-сервер Where to get connection data, step-by-step instructions for buying a VPS - + Де отримати дані для підключення: покрокові інструкції з придбання VPS @@ -2586,12 +2586,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Choose a VPN protocol - + Виберіть протокол VPN Skip setup - + Пропустити налаштування Set up a VPN yourself @@ -2649,7 +2649,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Cancel installation - Відмінити встановлення + Відмінити встановлення @@ -2693,7 +2693,7 @@ and will not be shared or disclosed to the Amnezia or any third parties The port must be in the range of 1 to 65535 - + Порт повинен бути в межах від 1 до 65535 @@ -2714,7 +2714,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Point the camera at the QR code and hold for a couple of seconds. - Наведіть камеру на QR-код і утримуйте її протягом декількох секунд. + Наведіть камеру на QR-код і утримуйте її протягом декількох секунд. @@ -2742,7 +2742,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Let's get started - + Почнемо @@ -2801,12 +2801,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. - + Увімкніть обфускацію WireGuard. Це може бути корисним, якщо WireGuard заблокований у вашого провайдера. Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. - + Використовуйте коди підключення тільки з джерел, яким ви довіряєте. Коди з публічних джерел можуть бути створені для перехоплення ваших даних. @@ -2856,7 +2856,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Config revoked - Кофігурацію відкликано + Кофігурацію відкликано @@ -2881,22 +2881,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Save AmneziaWG config - Зберегти AmneziaWG конфігурацію + Зберегти AmneziaWG конфігурацію Save Shadowsocks config - Зберегти конфігурацію Shadowsocks + Зберегти конфігурацію Shadowsocks Save Cloak config - Зберегти конфігурацію Cloak + Зберегти конфігурацію Cloak Save XRay config - + Зберегти конфігурацію XRay @@ -2906,22 +2906,22 @@ and will not be shared or disclosed to the Amnezia or any third parties AmneziaWG native format - нативний формат AmneziaWG + нативний формат AmneziaWG Shadowsocks native format - Shadowsocks нативний формат + Shadowsocks нативний формат Cloak native format - Cloak нативний формат + Cloak нативний формат XRay native format - + XRay нативний формат @@ -2931,48 +2931,48 @@ and will not be shared or disclosed to the Amnezia or any third parties Share full access to the server and VPN - Поділитись повним доступом до серверу + Поділитись повним доступом до серверу Use for your own devices, or share with those you trust to manage the server. - Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. + Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. Users - Користувачі + Користувачі User name - Ім'я користувача + Ім'я користувача Search - Пошук + Пошук Creation date: %1 - + Дата створення: %1 Latest handshake: %1 - + Останнє з'єднання: %1 Data received: %1 - + Отримано даних: %1 Data sent: %1 - + Відправлено даних: %1 Creation date: @@ -2981,42 +2981,42 @@ and will not be shared or disclosed to the Amnezia or any third parties Rename - Перейменувати + Перейменувати Client name - + Назва клієнта Save - Зберегти + Зберегти Revoke - Відкликати + Відкликати Revoke the config for a user - %1? - Відкликати доступ для користувача - %1? + Відкликати доступ для користувача - %1? The user will no longer be able to connect to your server. - Користувач більше не зможе підключатись до вашого сервера + Користувач більше не зможе підключатись до вашого сервера - + Continue - Продовжити + Продовжити Cancel - Відмінити + Відмінити Full access @@ -3051,49 +3051,49 @@ and will not be shared or disclosed to the Amnezia or any third parties Full access to the server and VPN - Повний доступ до серверу та VPN + Повний доступ до серверу та VPN We recommend that you use full access to the server only for your own additional devices. - Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. + Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. - Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. + Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. Server - Сервер + Сервер Accessing - Доступ + Доступ File with accessing settings to - Файл з налаштуваннями доступу до + Файл з налаштуваннями доступу до Share - Поділитись + Поділитись Connection to - Підключення до + Підключення до File with connection settings to - Файл з налаштуванням доступу до + Файл з налаштуванням доступу до @@ -3101,12 +3101,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Logging was disabled after 14 days, log files were deleted - + Логування було вимкнене через 14 днів, файли журналів були видалені Settings restored from backup file - Відновлення налаштувань із бекап файлу + Відновлення налаштувань із бекап файлу @@ -3639,22 +3639,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - Shadowsocks - маскує VPN-трафік під звичайний веб-трафік, але розпізнається системами аналізу трафіка в деяких регіонах з високим рівнем цензури. + Shadowsocks - маскує VPN-трафік під звичайний веб-трафік, але розпізнається системами аналізу трафіка в деяких регіонах з високим рівнем цензури. OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak - OpenVPN з маскуванням VPN під HTTPS трафік і захистом від active-probing. Підходить для регіонів з самим високим рівнем цензури. + OpenVPN over Cloak - OpenVPN з маскуванням VPN під HTTPS трафік і захистом від active-probing. Підходить для регіонів з самим високим рівнем цензури. IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. - + IKEv2/IPsec — сучасний стабільний протокол, який дещо швидший за інші та відновлює підключення після втрати сигналу. Має нативну підтримку на останніх версіях Android та iOS. Create a file vault on your server to securely store and transfer files. - Створіть на сервері файлове сховище для безпечного зберігання та передачі файлів. + Створіть на сервері файлове сховище для безпечного зберігання та передачі файлів. @@ -3676,7 +3676,24 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - + Це комбінація протоколу OpenVPN та плагіна Cloak, розроблена спеціально для захисту від блокувань. + +OpenVPN забезпечує безпечне VPN-підключення шляхом шифрування всього інтернет-трафіку між клієнтом і сервером. + +Cloak захищає OpenVPN від виявлення та блокування. + +Cloak може змінювати метадані пакетів так, що повністю маскує VPN-трафік як звичайний веб-трафік і також захищає VPN від виявлення за допомогою активного сканування. Це робить його дуже стійким до виявлення. + +Одразу після отримання першого пакета даних Cloak аутентифікує вхідне підключення. Якщо аутентифікація не вдається, плагін маскує сервер як фальшивий вебсайт, і ваш VPN стає невидимим для систем аналізу. + +Якщо у вашому регіоні екстремальний рівень інтернет-цензури, ми радимо використовувати тільки OpenVPN через Cloak з самого початку роботи з додатком. + + Доступний в AmneziaVPN на всіх платформах + Високе споживання енергії на мобільних пристроях + Гнучкі налаштування + Не розпізнається системами аналізу DPI + Працює через TCP мережевий протокол, порт 443. + @@ -3689,7 +3706,16 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - + Відносно новий популярний VPN-протокол з спрощеною архітектурою. +WireGuard забезпечує стабільне VPN-підключення та високу продуктивність на всіх пристроях. Він використовує жорстко закодовані налаштування шифрування. Порівняно з OpenVPN, WireGuard має нижчу затримку та кращу пропускну здатність передачі даних. + +WireGuard дуже чутливий до блокувань через свої чіткі підписи пакетів. На відміну від деяких інших VPN-протоколів, які використовують техніки обфускації, постійні шаблони підписів пакетів WireGuard легше ідентифікуються та можуть бути заблоковані просунутими системами глибокого аналізу пакетів (DPI) та іншими інструментами моніторингу мережі. + +* Доступний в AmneziaVPN на всіх платформах +* Низьке споживання енергії +* Мінімальна кількість налаштувань +* Легко розпізнається системами аналізу DPI, схильний до блокування +* Працює через UDP мережевий протокол. @@ -3697,7 +3723,10 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - + Протокол **REALITY**, сучасна розробка XRay. Спеціально розроблений для протидії найвищим рівням інтернет-цензури завдяки новому підходу до маскування. +REALITY унікально ідентифікує цензорів під час фази TLS-handshake, працюючи як проксі для VPN клієнтів, при цьому перенаправляючи цензорів на справжні вебсайти, такі як google.com, надаючи справжній TLS-сертифікат та інші дані. +Цей функціонал, відрізняє REALITY від подібних технологій, своєю здатністю маскувати веб-трафік у такий такий, що походить із випадкових справжніх сайтів без необхідності спеціальних налаштувань. +На відміну від старіших протоколів, таких як VMess, VLESS та XTLS-Vision transport, продвиуте розпізнавання "Свій — Чужий" REALITY під час TLS-handshake підвищує безпеку та протидіє виявленню складними системами DPI, що використовують активні техніки аналізу. Це робить REALITY надійним рішенням для підтримання інтернет-свободи в середовищах з жорсткою цензурою. @@ -3759,7 +3788,7 @@ While it offers a blend of security, stability, and speed, it's essential t XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - + XRay with REALITY — підходить для країн з найвищим рівнем інтернет-цензури. Маскування трафіку під веб-трафік на рівні TLS. Захист від виявлення активними методами сканування (active-probing). IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. @@ -3819,7 +3848,14 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for For more detailed information, you can find it in the support section under "Create SFTP file storage." - + Після встановлення Amnezia створить + + файл-сховище на вашому сервері. Ви зможете отримати + доступ до нього за допомогою FileZilla та інших SFTP-клієнтів, +а також змонтувати диск на вашому пристрої для безпосереднього доступу до нього. + +Для більш детальної інформації зверніться + до розділу підтримки під заголовком «Створення SFTP файл-сховища». This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for blocking protection. @@ -3917,7 +3953,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SOCKS5 proxy server - + SOCKS5 proxy server @@ -4172,12 +4208,12 @@ This means that AmneziaWG keeps the fast performance of the original while addin Copy config string - Скопіювати стрічку конфігурації + Скопіювати стрічку конфігурації Show connection settings - Показати налаштування підключення + Показати налаштування підключення From 175477d31f6bd17d5ee8f5f020c2589ab00fcf6b Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 9 Sep 2024 14:36:33 +0300 Subject: [PATCH 3/7] Android qt 6.7 (#1024) * Up Gradle to 8.10 * Update Android dependencies * Up Qt to 6.7.2 * Up qtkeychain to 0.14.3 * Move function of changing the color of the navigation bar to the android side * Fix splashscreen and recent apps thumbnail backgrounds * Android authentication refactoring * Fix GitHub action * Fix the extra circle around the connect button on Android * Fix keyboard popup * Increase the amount of requestNetwork attempts on Android 11 --- .github/workflows/deploy.yml | 14 +-- client/3rd/qtkeychain | 2 +- client/android/AndroidManifest.xml | 13 ++- client/android/build.gradle | 3 + client/android/build.gradle.kts | 2 + client/android/gradle/libs.versions.toml | 22 ++-- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- client/android/gradlew | 7 +- client/android/gradlew.bat | 22 ++-- client/android/qt/build.gradle.kts | 2 +- client/android/res/values/styles.xml | 3 + client/android/settings.gradle.kts | 2 +- .../src/org/amnezia/vpn/AmneziaActivity.kt | 23 +++++ .../src/org/amnezia/vpn/AuthActivity.kt | 97 ++++++++++++++++++ .../src/org/amnezia/vpn/AuthHelper.java | 24 ----- .../org/amnezia/vpn/ImportConfigActivity.kt | 4 +- .../org/amnezia/vpn/qt/QtAndroidController.kt | 2 + .../utils/src/main/kotlin/net/NetworkState.kt | 2 +- client/cmake/android.cmake | 2 - .../platforms/android/android_controller.cpp | 30 ++++++ client/platforms/android/android_controller.h | 4 + .../platforms/android/authResultReceiver.cpp | 16 --- client/platforms/android/authResultReceiver.h | 32 ------ client/ui/controllers/exportController.cpp | 29 ------ client/ui/controllers/exportController.h | 11 -- client/ui/controllers/pageController.cpp | 23 +---- client/ui/controllers/settingsController.cpp | 9 ++ client/ui/controllers/settingsController.h | 2 + client/ui/controllers/systemController.cpp | 9 ++ client/ui/controllers/systemController.h | 1 + client/ui/qml/Components/ConnectButton.qml | 11 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 21 ++-- 33 files changed, 256 insertions(+), 192 deletions(-) create mode 100644 client/android/src/org/amnezia/vpn/AuthActivity.kt delete mode 100644 client/android/src/org/amnezia/vpn/AuthHelper.java delete mode 100644 client/platforms/android/authResultReceiver.cpp delete mode 100644 client/platforms/android/authResultReceiver.h diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 220fbd79..be4d068b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -297,24 +297,24 @@ jobs: env: ANDROID_BUILD_PLATFORM: android-34 - QT_VERSION: 6.6.2 + QT_VERSION: 6.7.2 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} steps: - name: 'Install desktop Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' target: 'desktop' - arch: 'gcc_64' + arch: 'linux_gcc_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_x86_64 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -325,7 +325,7 @@ jobs: extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_x86 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -336,7 +336,7 @@ jobs: extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_armv7 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -347,7 +347,7 @@ jobs: extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_arm64_v8a Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' diff --git a/client/3rd/qtkeychain b/client/3rd/qtkeychain index 74776e2a..7460df6a 160000 --- a/client/3rd/qtkeychain +++ b/client/3rd/qtkeychain @@ -1 +1 @@ -Subproject commit 74776e2a3e2d98d19943e0968901c5b5e04cc1bd +Subproject commit 7460df6a978669290de5b56c2d98b199b61c3f88 diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index c1c40b52..179def86 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -3,7 +3,6 @@ @@ -46,7 +45,7 @@ android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density |fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc" android:launchMode="singleInstance" - android:windowSoftInputMode="adjustResize" + android:windowSoftInputMode="stateUnchanged|adjustResize" android:exported="true"> @@ -68,9 +67,6 @@ android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --" /> - + + NyAue(f;$XO2=4Cg1P$=#e%|lo zKk1`B>Q#GH)wNd-&cJog!qw7YfYndTeo)CyX{fOHsQjGa<{e=jamMNwjdatD={CN3>GNchOE9OGPIqr)3v>RcKWR3Z zF-guIMjE2UF0Wqk1)21791y#}ciBI*bAenY*BMW_)AeSuM5}vz_~`+1i!Lo?XAEq{TlK5-efNFgHr6o zD>^vB&%3ZGEWMS>`?tu!@66|uiDvS5`?bF=gIq3rkK(j<_TybyoaDHg8;Y#`;>tXI z=tXo~e9{U!*hqTe#nZjW4z0mP8A9UUv1}C#R*@yu9G3k;`Me0-BA2&Aw6f`{Ozan2 z8c8Cs#dA-7V)ZwcGKH}jW!Ja&VaUc@mu5a@CObzNot?b{f+~+212lwF;!QKI16FDS zodx>XN$sk9;t;)maB^s6sr^L32EbMV(uvW%or=|0@U6cUkE`_!<=LHLlRGJx@gQI=B(nn z-GEjDE}*8>3U$n(t^(b^C$qSTI;}6q&ypp?-2rGpqg7b}pyT zOARu2x>0HB{&D(d3sp`+}ka+Pca5glh|c=M)Ujn_$ly^X6&u z%Q4Y*LtB_>i6(YR!?{Os-(^J`(70lZ&Hp1I^?t@~SFL1!m0x6j|NM!-JTDk)%Q^R< z@e?23FD&9_W{Bgtr&CG&*Oer3Z(Bu2EbV3T9FeQ|-vo5pwzwQ%g&=zFS7b{n6T2ZQ z*!H(=z<{D9@c`KmHO&DbUIzpg`+r5207}4D=_P$ONIc5lsFgn)UB-oUE#{r+|uHc^hzv_df zV`n8&qry%jXQ33}Bjqcim~BY1?KZ}x453Oh7G@fA(}+m(f$)TY%7n=MeLi{jJ7LMB zt(mE*vFnep?YpkT_&WPV9*f>uSi#n#@STJmV&SLZnlLsWYI@y+Bs=gzcqche=&cBH2WL)dkR!a95*Ri)JH_4c*- zl4pPLl^as5_y&6RDE@@7342DNyF&GLJez#eMJjI}#pZN{Y8io{l*D+|f_Y&RQPia@ zNDL;SBERA|B#cjlNC@VU{2csOvB8$HzU$01Q?y)KEfos>W46VMh>P~oQC8k=26-Ku)@C|n^zDP!hO}Y z_tF}0@*Ds!JMt>?4y|l3?`v#5*oV-=vL7}zehMON^=s1%q+n=^^Z{^mTs7}*->#YL z)x-~SWE{e?YCarwU$=cS>VzmUh?Q&7?#Xrcce+jeZ|%0!l|H_=D_`77hBfd4Zqk&! zq-Dnt_?5*$Wsw8zGd@?woEtfYZ2|9L8b>TO6>oMh%`B7iBb)-aCefM~q|S2Cc0t9T zlu-ZXmM0wd$!gd-dTtik{bqyx32%f;`XUvbUWWJmpHfk8^PQIEsByJm+@+-aj4J#D z4#Br3pO6z1eIC>X^yKk|PeVwX_4B+IYJyJyc3B`4 zPrM#raacGIzVOexcVB;fcsxS=s1e&V;Xe$tw&KQ`YaCkHTKe*Al#velxV{3wxx}`7@isG zp6{+s)CG%HF#JBAQ_jM%zCX5X;J%-*%&jVI?6KpYyzGbq7qf;&hFprh?E5Wyo=bZ) z8YNycvMNGp1836!-?nihm6jI`^C`EeGryoNZO1AFTQhzFJOA%Q{X(sMYlzABt!&f{ zoDENSuoJQIg5Q#@BUsNJX2h>jkdx4<+ipUymWKFr;w+s>$laIIkfP6nU}r+?J9bZg zUIxz>RX$kX=C4m(zh-Eg$BsJ4OL&_J38PbHW&7JmR27%efAkqqdvf)Am)VF$+U3WR z-E#I9H6^)zHLKCs7|Zs<7Bo9VCS3@CDQ;{UTczoEprCKL3ZZW!ffmZFkcWU-V|_M2 zUA9~8tE9<5`59W-UgUmDFp11YlORl3mS3*2#ZHjv{*-1#uMV_oVTy{PY(}AqZv#wF zJVks)%N6LaHF$$<6p8S8Lqn+5&t}DmLKiC~lE{jPZ39oj{wR&fe*LX-z0m}9ZnZ{U z>3-5Bh{KKN^n5i!M79Aw5eY=`6fG#aW1_ZG;fw7JM69qk^*(rmO{|Z6rXy?l=K=#_ zE-zd*P|(sskasO(cZ5L~_{Mz&Y@@@Q)5_8l<6vB$@226O+pDvkFaK8b>%2 zfMtgJ@+cN@w>3)(_uR;s8$sGONbYvoEZ3-)zZk4!`tNzd<0lwt{RAgplo*f@Z)uO` zzd`ljSqKfHJOLxya4_}T`k5Ok1Mpo#MSqf~&ia3uIy{zyuaF}pV6 z)@$ZG5LYh8Gge*LqM_|GiT1*J*uKes=Oku_gMj&;FS`*sfpM+ygN&yOla-^WtIU#$ zuw(_-?DS?6DY7IbON7J)p^IM?N>7x^3)(7wR4PZJu(teex%l>zKAUSNL@~{czc}bR z)I{XzXqZBU3a;7UQ~PvAx8g-3q-9AEd}1JrlfS8NdPc+!=HJ6Bs( zCG!0;e0z-22(Uzw>hkEmC&xj?{0p|kc zM}MMXCF%RLLa#5jG`+}{pDL3M&|%3BlwOi?dq!)KUdv5__zR>u^o|QkYiqr(m3HxF z6J*DyN#Jpooc$ok=b7{UAVM@nwGsr6kozSddwulf5g1{B=0#2)zv!zLXQup^BZ4sv*sEsn)+MA?t zEL)}3*R?4(J~CpeSJPM!oZ~8;8s_=@6o`IA%{aEA9!GELRvOuncE`s7sH91 zmF=+T!Q6%){?lJn3`5}oW31(^Of|$r%`~gT{eimT7R~*Mg@x+tWM3KE>=Q>nkMG$U za7r>Yz2LEaA|PsMafvJ(Y>Xzha?=>#B!sYfVob4k5Orb$INFdL@U0(J8Hj&kgWUlO zPm+R07E+oq^4f4#HvEPANGWLL_!uF{nkHYE&BCH%l1FL_r(Nj@M)*VOD5S42Gk-yT z^23oAMvpA57H(fkDGMx86Z}rtQhR^L!T2iS!788E z+^${W1V}J_NwdwdxpXAW8}#6o1(Uu|vhJvubFvQIH1bDl4J4iDJ+181KuDuHwvM?` z%1@Tnq+7>p{O&p=@QT}4wT;HCb@i)&7int<0#bj8j0sfN3s6|a(l7Bj#7$hxX@~iP z1HF8RFH}irky&eCN4T94VyKqGywEGY{Gt0Xl-`|dOU&{Q;Ao;sL>C6N zXx1y^RZSaL-pG|JN;j9ADjo^XR}gce#seM4QB1?S`L*aB&QlbBIRegMnTkTCks7JU z<0(b+^Q?HN1&$M1l&I@>HMS;!&bb()a}hhJzsmB?I`poqTrSoO>m_JE5U4=?o;OV6 zBZjt;*%1P>%2{UL=;a4(aI>PRk|mr&F^=v6Fr&xMj8fRCXE5Z2qdre&;$_RNid5!S zm^XiLK25G6_j4dWkFqjtU7#s;b8h?BYFxV?OE?c~&ME`n`$ix_`mb^AWr+{M9{^^Rl;~KREplwy2q;&xe zUR0SjHzKVYzuqQ84w$NKVPGVHL_4I)Uw<$uL2-Ml#+5r2X{LLqc*p13{;w#E*Kwb*1D|v?e;(<>vl@VjnFB^^Y;;b3 z=R@(uRj6D}-h6CCOxAdqn~_SG=bN%^9(Ac?zfRkO5x2VM0+@_qk?MDXvf=@q_* z3IM@)er6-OXyE1Z4sU3{8$Y$>8NcnU-nkyWD&2ZaqX1JF_JYL8y}>@V8A5%lX#U3E zet5PJM`z79q9u5v(OE~{by|Jzlw2<0h`hKpOefhw=fgLTY9M8h+?37k@TWpzAb2Fc zQMf^aVf!yXlK?@5d-re}!fuAWu0t57ZKSSacwRGJ$0uC}ZgxCTw>cjRk*xCt%w&hh zoeiIgdz__&u~8s|_TZsGvJ7sjvBW<(C@}Y%#l_ID2&C`0;Eg2Z+pk;IK}4T@W6X5H z`s?ayU-iF+aNr5--T-^~K~p;}D(*GWOAYDV9JEw!w8ZYzS3;W6*_`#aZw&9J ziXhBKU3~zd$kKzCAP-=t&cFDeQR*_e*(excIUxKuD@;-twSlP6>wWQU)$|H3Cy+`= z-#7OW!ZlYzZxkdQpfqVDFU3V2B_-eJS)Fi{fLtRz!K{~7TR~XilNCu=Z;{GIf9KYz zf3h=Jo+1#_s>z$lc~e)l93h&RqW1VHYN;Yjwg#Qi0yzjN^M4cuL>Ew`_-_wRhi*!f zLK6vTpgo^Bz?8AsU%#n}^EGigkG3FXen3M;hm#C38P@Zs4{!QZPAU=m7ZV&xKI_HWNt90Ef zxClm)ZY?S|n**2cNYy-xBlLAVZ=~+!|7y`(fh+M$#4zl&T^gV8ZaG(RBD!`3?9xcK zp2+aD(T%QIgrLx5au&TjG1AazI;`8m{K7^!@m>uGCSR;Ut{&?t%3AsF{>0Cm(Kf)2 z?4?|J+!BUg*P~C{?mwPQ#)gDMmro20YVNsVx5oWQMkzQ? zsQ%Y>%7_wkJqnSMuZjB9lBM(o zWut|B7w48cn}4buUBbdPBW_J@H7g=szrKEpb|aE>!4rLm+sO9K%iI75y~2HkUo^iw zJ3se$8$|W>3}?JU@3h@M^HEFNmvCp|+$-0M?RQ8SMoZ@38%!tz8f8-Ptb@106heiJ z^Bx!`0=Im z1!NUhO=9ICM*+||b3a7w*Y#5*Q}K^ar+oMMtekF0JnO>hzHqZKH0&PZ^^M(j;vwf_ z@^|VMBpcw8;4E-9J{(u7sHSyZpQbS&N{VQ%ZCh{c1UA5;?R} z+52*X_tkDQ(s~#-6`z4|Y}3N#a&dgP4S_^tsV=oZr4A1 zaSoPN1czE(UIBrC_r$0HM?RyBGe#lTBL4~JW#A`P^#0wuK)C-2$B6TvMi@@%K@JAT_IB^T7Zfqc8?{wHcSVG_?{(wUG%zhCm=%qP~EqeqKI$9UivF zv+5IUOs|%@ypo6b+i=xsZ=^G1yeWe)z6IX-EC`F=(|_GCNbHbNp(CZ*lpSu5n`FRA zhnrc4w+Vh?r>her@Ba_jv0Omp#-H7avZb=j_A~B%V0&FNi#!S8cwn0(Gg-Gi_LMI{ zCg=g@m{W@u?GQ|yp^yENd;M=W2s-k7Gw2Z(tsD5fTGF{iZ%Ccgjy6O!AB4x z%&=6jB7^}pyftW2YQpOY1w@%wZy%}-l0qJlOSKZXnN2wo3|hujU+-U~blRF!^;Tan z0w;Srh0|Q~6*tXf!5-rCD)OYE(%S|^WTpa1KHtpHZ{!;KdcM^#g8Z^+LkbiBHt85m z;2xv#83lWB(kplfgqv@ZNDcHizwi4-8+WHA$U-HBNqsZ`hKcUI3zV3d1ngJP-AMRET*A{> zb2A>Fk|L|WYV;Eu4>{a6ESi2r3aZL7x}eRc?cf|~bP)6b7%BnsR{Sa>K^0obn?yiJ zCVvaZ&;d_6WEk${F1SN0{_`(#TuOOH1as&#&xN~+JDzX(D-WU_nLEI}T_VaeLA=bc zl_UZS$nu#C1yH}YV>N2^9^zye{rDrn(rS99>Fh&jtNY7PP15q%g=RGnxACdCov47= zwf^9zfJaL{y`R#~tvVL#*<`=`Qe zj_@Me$6sIK=LMFbBrJps7vdaf_HeX?eC+P^{AgSvbEn?n<}NDWiQGQG4^ZOc|GskK z$Ve2_n8gQ-KZ=s(f`_X!+vM5)4+QmOP()2Fe#IL2toZBf+)8gTVgDSTN1CkP<}!j7 z0SEl>PBg{MnPHkj4wj$mZ?m5x!1ePVEYI(L_sb0OZ*=M%yQb?L{UL(2_*CTVbRxBe z@{)COwTK1}!*CK0Vi4~AB;HF(MmQf|dsoy(eiQ>WTKcEQlnKOri5xYsqi61Y=I4kzAjn5~{IWrz_l))|Ls zvq7xgQs?Xx@`N?f7+3XKLyD~6DRJw*uj*j?yvT3}a;(j_?YOe%hUFcPGWRVBXzpMJ zM43g6DLFqS9tcTLSg=^&N-y0dXL816v&-nqC0iXdg7kV|PY+js`F8dm z2PuHw&k+8*&9SPQ6f!^5q0&AH(i+z3I7a?8O+S5`g)>}fG|BM&ZnmL;rk)|u{1!aZ zEZHpAMmK_v$GbrrWNP|^2^s*!0waLW=-h5PZa-4jWYUt(Hr@EA(m3Mc3^uDxwt-me^55FMA9^>hpp26MhqjLg#^Y7OIJ5%ZLdNx&uDgIIqc zZRZl|n6TyV)0^DDyVtw*jlWkDY&Gw4q;k!UwqSL6&sW$B*5Rc?&)dt29bDB*b6IBY z6SY6Unsf6AOQdEf=P1inu6(6hVZ0~v-<>;LAlcQ2u?wRWj5VczBT$Op#8IhppP-1t zfz5H59Aa~yh7EN;BXJsLyjkjqARS5iIhDVPj<=4AJb}m6M@n{xYj3qsR*Q8;hVxDyC4vLI;;?^eENOb5QARj#nII5l$MtBCI@5u~(ylFi$ zw6-+$$XQ}Ca>FWT>q{k)g{Ml(Yv=6aDfe?m|5|kbGtWS}fKWI+})F6`x@||0oJ^(g|+xi zqlPdy5;`g*i*C=Q(aGeDw!eQg&w>UUj^{o?PrlFI=34qAU2u@BgwrBiaM8zoDTFJ< zh7nWpv>dr?q;4ZA?}V}|7qWz4W?6#S&m>hs4IwvCBe@-C>+oohsQZ^JC*RfDRm!?y zS4$7oxcI|##ga*y5hV>J4a%HHl^t$pjY%caL%-FlRb<$A$E!ws?8hf0@(4HdgQ!@> zds{&g$ocr9W4I84TMa9-(&^_B*&R%^=@?Ntxi|Ejnh;z=!|uVj&3fiTngDPg=0=P2 zB)3#%HetD84ayj??qrxsd9nqrBem(8^_u_UY{1@R_vK-0H9N7lBX5K(^O2=0#TtUUGSz{ z%g>qU8#a$DyZ~EMa|8*@`GOhCW3%DN%xuS91T7~iXRr)SG`%=Lfu%U~Z_`1b=lSi?qpD4$vLh$?HU6t0MydaowUpb zQr{>_${AMesCEffZo`}K0^~x>RY_ZIG{(r39MP>@=aiM@C;K)jUcfQV8#?SDvq>9D zI{XeKM%$$XP5`7p3K0T}x;qn)VMo>2t}Ib(6zui;k}<<~KibAb%p)**e>ln<=qyWU zrRDy|UXFi9y~PdEFIAXejLA{K)6<)Q`?;Q5!KsuEw({!#Rl8*5_F{TP?u|5(Hijv( ztAA^I5+$A*+*e0V0R~fc{ET-RAS3suZ}TRk3r)xqj~g_hxB`qIK5z(5wxYboz%46G zq{izIz^5xW1Vq#%lhXaZL&)FJWp0VZNO%2&ADd?+J%K$fM#T_Eke1{dQsx48dUPUY zLS+DWMJeUSjYL453f@HpRGU6Dv)rw+-c6xB>(=p4U%}_p>z^I@Ow9`nkUG21?cMIh9}hN?R-d)*6%pr6d@mcb*ixr7 z)>Lo<&2F}~>WT1ybm^9UO{6P9;m+fU^06_$o9gBWL9_}EMZFD=rLJ~&e?fhDnJNBI zKM=-WR6g7HY5tHf=V~6~QIQ~rakNvcsamU8m28YE=z8+G7K=h%)l6k zmCpiDInKL6*e#)#Pt;ANmjf`8h-nEt&d}(SBZMI_A{BI#ck-_V7nx)K9_D9K-p@?Zh81#b@{wS?wCcJ%og)8RF*-0z+~)6f#T` zWqF7_CBcnn=S-1QykC*F0YTsKMVG49BuKQBH%WuDkEy%E?*x&tt%0m>>5^HCOq|ux zuvFB)JPR-W|%$24eEC^AtG3Gp4qdK%pjRijF5Sg3X}uaKEE z-L5p5aVR!NTM8T`4|2QA@hXiLXRcJveWZ%YeFfV%mO5q#($TJ`*U>hicS+CMj%Ip# zivoL;dd*araeJK9EA<(tihD50FHWbITBgF9E<33A+eMr2;cgI3Gg6<-2o|_g9|> zv5}i932( zYfTE9?4#nQhP@a|zm#9FST2 z!y+p3B;p>KkUzH!K;GkBW}bWssz)9b>Ulg^)EDca;jDl+q=243BddS$hY^fC6lbpM z(q_bo4V8~eVeA?0LFD6ZtKcmOH^75#q$Eo%a&qvE8Zsqg=$p}u^|>DSWUP5i{6)LAYF4E2DfGZuMJ zMwxxmkxQf}Q$V3&2w|$`9_SQS^2NVbTHh;atB>=A%!}k-f4*i$X8m}Ni^ppZXk5_oYF>Gq(& z0wy{LjJOu}69}~#UFPc;$7ka+=gl(FZCy4xEsk);+he>Nnl>hb5Ud-lj!CNicgd^2 z_Qgr_-&S7*#nLAI7r()P$`x~fy)+y=W~6aNh_humoZr7MWGSWJPLk}$#w_1n%(@? z3FnHf1lbxKJbQ9c&i<$(wd{tUTX6DAKs@cXIOBv~!9i{wD@*|kwfX~sjKASrNFGvN zrFc=!0Bb^OhR2f`%hrp2ibv#KUxl)Np1aixD9{^o=)*U%n%rTHX?FSWL^UGpHpY@7 z74U}KoIRwxI#>)Pn4($A`nw1%-D}`sGRZD8Z#lF$6 zOeA5)+W2qvA%m^|$WluUU-O+KtMqd;Pd58?qZj})MbxYGO<{z9U&t4D{S2G>e+J9K ztFZ?}ya>SVOLp9hpW)}G%kTrg*KXXXsLkGdgHb+R-ZXqdkdQC0_)`?6mqo8(EU#d( zy;u&aVPe6C=YgCRPV!mJ6R6kdY*`e+VGM~`VtC>{k27!9vAZT)x2~AiX5|m1Rq}_= z;A9LX^nd$l-9&2%4s~p5r6ad-siV`HtxKF}l&xGSYJmP=z!?Mlwmwef$EQq~7;#OE z)U5eS6dB~~1pkj#9(}T3j!((8Uf%!W49FfUAozijoxInUE7z`~U3Y^}xc3xp){#9D z<^Tz2xw}@o@fdUZ@hnW#dX6gDOj4R8dV}Dw`u!h@*K)-NrxT8%2`T}EvOImNF_N1S zy?uo6_ZS>Qga4Xme3j#aX+1qdFFE{NT0Wfusa$^;eL5xGE_66!5_N8!Z~jCAH2=${ z*goHjl|z|kbmIE{cl-PloSTtD+2=CDm~ZHRgXJ8~1(g4W=1c3=2eF#3tah7ho`zm4 z05P&?nyqq$nC?iJ-nK_iBo=u5l#|Ka3H7{UZ&O`~t-=triw=SE7ynzMAE{Mv-{7E_ zViZtA(0^wD{iCCcg@c{54Ro@U5p1QZq_XlEGtdBAQ9@nT?(zLO0#)q55G8_Ug~Xnu zR-^1~hp|cy&52iogG@o?-^AD8Jb^;@&Ea5jEicDlze6%>?u$-eE};bQ`T6@(bED0J zKYtdc?%9*<<$2LCBzVx9CA4YV|q-qg*-{yQ;|0=KIgI6~z0DKTtajw2Oms3L zn{C%{P`duw!(F@*P)lFy11|Z&x`E2<=$Ln38>UR~z6~za(3r;45kQK_^QTX%!s zNzoIFFH8|Y>YVrUL5#mgA-Jh>j7)n)5}iVM4%_@^GSwEIBA2g-;43* z*)i7u*xc8jo2z8&=8t7qo|B-rsGw)b8UXnu`RgE4u!(J8yIJi(5m3~aYsADcfZ!GG zzqa7p=sg`V_KjiqI*LA-=T;uiNRB;BZZ)~88 z`C%p8%hIev2rxS12@doqsrjgMg3{A&N8A?%Ui5vSHh7!iC^ltF&HqG~;=16=h0{ygy^@HxixUb1XYcR36SB}}o3nxu z_IpEmGh_CK<+sUh@2zbK9MqO!S5cao=8LSQg0Zv4?ju%ww^mvc0WU$q@!oo#2bv24 z+?c}14L2vlDn%Y0!t*z=$*a!`*|uAVu&NO!z_arim$=btpUPR5XGCG0U3YU`v>yMr z^zmTdcEa!APX zYF>^Q-TP11;{VgtMqC}7>B^2gN-3KYl33gS-p%f!X<_Hr?`rG8{jb9jmuQA9U;BeG zHj6Pk(UB5c6zwX%SNi*Py*)gk^?+729$bAN-EUd*RKN7{CM4`Q65a1qF*-QWACA&m zrT)B(M}yih{2r!Tiv5Y&O&=H_OtaHUz96Npo_k0eN|!*s2mLe!Zkuv>^E8Xa43ZwH zOI058AZznYGrRJ+`*GmZzMi6yliFmGMge6^j?|PN%ARns!Eg$ufpcLc#1Ns!1@1 zvC7N8M$mRgnixwEtX{ypBS^n`k@t2cCh#_6L6WtQb8E~*Vu+Rr)YsKZRX~hzLG*BE zaeU#LPo?RLm(Wzltk79Jd1Y$|6aWz1)wf1K1RtqS;qyQMy@H@B805vQ%wfSJB?m&&=^m4i* zYVH`zTTFbFtNFkAI`Khe4e^CdGZw;O0 zqkQe2|NG_y6D%h(|EZNf&77_!NU%0y={^E=*gKGQ=)LdKPM3zUlM@otH2X07Awv8o zY8Y7a1^&Yy%b%m{mNQ5sWNMTIq96Wtr>a(hL>Qi&F(ckgKkyvM0IH<_}v~Fv-GqDapig=3*ZMOx!%cYY)SKzo7ECyem z9Mj3C)tCYM?C9YIlt1?zTJXNOo&oVxu&uXKJs7i+j8p*Qvu2PAnY}b`KStdpi`trk ztAO}T8eOC%x)mu+4ps8sYZ=vYJp16SVWEEgQyFKSfWQ@O5id6GfL`|2<}hMXLPszS zgK>NWOoR zBRyKeUPevpqKKShD|MZ`R;~#PdNMB3LWjqFKNvH9k+;(`;-pyXM55?qaji#nl~K8m z_MifoM*W*X9CQiXAOH{cZcP0;Bn10E1)T@62Um>et2ci!J2$5-_HPy(AGif+BJpJ^ ziHWynC_%-NlrFY+(f7HyVvbDIM$5ci_i3?22ZkF>Y8RPBhgx-7k3M2>6m5R24C|~I z&RPh9xpMGzhN4bii*ryWaN^d(`0 zTOADlU)g`1p+SVMNLztd)c+;XjXox(VHQwqzu>FROvf0`s&|NEv26}(TAe;@=FpZq zaVs6mp>W0rM3Qg*6x5f_bPJd!6dQGmh?&v0rpBNfS$DW-{4L7#_~-eA@7<2BsZV=X zow){3aATmLZOQrs>uzDkXOD=IiX;Ue*B(^4RF%H zeaZ^*MWn4tBDj(wj114r(`)P96EHq4th-;tWiHhkp2rDlrklX}I@ib-nel0slFoQO zOeTc;Rh7sMIebO`1%u)=GlEj+7HU;c|Nj>2j)J-kpR)s3#+9AiB zd$hAk6;3pu9(GCR#)#>aCGPYq%r&i02$0L9=7AlIGYdlUO5%eH&M!ZWD&6^NBAj0Y9ZDcPg@r@8Y&-}e!aq0S(`}NuQ({;aigCPnq75U9cBH&Y7 ze)W0aD>muAepOKgm7uPg3Dz7G%)nEqTUm_&^^3(>+eEI;$ia`m>m0QHEkTt^=cx^JsBC68#H(3zc~Z$E9I)oSrF$3 zUClHXhMBZ|^1ikm3nL$Z@v|JRhud*IhOvx!6X<(YSX(9LG#yYuZeB{=7-MyPF;?_8 zy2i3iVKG2q!=JHN>~!#Bl{cwa6-yB@b<;8LSj}`f9pw7#x3yTD>C=>1S@H)~(n_K4 z2-yr{2?|1b#lS`qG@+823j;&UE5|2+EdU4nVw5=m>o_gj#K>>(*t=xI7{R)lJhLU{ z4IO6!x@1f$aDVIE@1a0lraN9!(j~_uGlks)!&davUFRNYHflp<|ENwAxsp~4Hun$Q z$w>@YzXp#VX~)ZP8`_b_sTg(Gt7?oXJW%^Pf0UW%YM+OGjKS}X`yO~{7WH6nX8S6Z ztl!5AnM2Lo*_}ZLvo%?iV;D2z>#qdpMx*xY2*GGlRzmHCom`VedAoR=(A1nO)Y>;5 zCK-~a;#g5yDgf7_phlkM@)C8s!xOu)N2UnQhif-v5kL$*t=X}L9EyBRq$V(sI{90> z=ghTPGswRVbTW@dS2H|)QYTY&I$ljbpNPTc_T|FEJkSW7MV!JM4I(ksRqQ8)V5>}v z2Sf^Z9_v;dKSp_orZm09jb8;C(vzFFJgoYuWRc|Tt_&3k({wPKiD|*m!+za$(l*!gNRo{xtmqjy1=kGzFkTH=Nc>EL@1Um0BiN1)wBO$i z6rG={bRcT|%A3s3xh!Bw?=L&_-X+6}L9i~xRj2}-)7fsoq0|;;PS%mcn%_#oV#kAp zGw^23c8_0~ ze}v9(p};6HM0+qF5^^>BBEI3d=2DW&O#|(;wg}?3?uO=w+{*)+^l_-gE zSw8GV=4_%U4*OU^hibDV38{Qb7P#Y8zh@BM9pEM_o2FuFc2LWrW2jRRB<+IE)G=Vx zuu?cp2-`hgqlsn|$nx@I%TC!`>bX^G00_oKboOGGXLgyLKXoo$^@L7v;GWqfUFw3< zekKMWo0LR;TaFY}Tt4!O$3MU@pqcw!0w0 zA}SnJ6Lb597|P5W8$OsEHTku2Kw9y4V=hx*K%iSn!#LW9W#~OiWf^dXEP$^2 zaok=UyGwy3GRp)bm6Gqr>8-4h@3=2`Eto2|JE6Sufh?%U6;ut1v1d@#EfcQP2chCt z+mB{Bk5~()7G>wM3KYf7Xh?LGbwg1uWLotmc_}Z_o;XOUDyfU?{9atAT$={v82^w9 z(MW$gINHt4xB3{bdbhRR%T}L?McK?!zkLK3(e>zKyei(yq%Nsijm~LV|9mll-XHavFcc$teX7v);H>=oN-+E_Q{c|! zp
    JV~-9AH}jxf6IF!PxrB9is{_9s@PYth^`pb%DkwghLdAyDREz(csf9)HcVRq z+2Vn~>{(S&_;bq_qA{v7XbU?yR7;~JrLfo;g$Lkm#ufO1P`QW_`zWW+4+7xzQZnO$ z5&GyJs4-VGb5MEDBc5=zxZh9xEVoY(|2yRv&!T7LAlIs@tw+4n?v1T8M>;hBv}2n) zcqi+>M*U@uY>4N3eDSAH2Rg@dsl!1py>kO39GMP#qOHipL~*cCac2_vH^6x@xmO|E zkWeyvl@P$2Iy*mCgVF+b{&|FY*5Ygi8237i)9YW#Fp& z?TJTQW+7U)xCE*`Nsx^yaiJ0KSW}}jc-ub)8Z8x(|K7G>`&l{Y&~W=q#^4Gf{}aJ%6kLXsmv6cr=Hi*uB`V26;dr4C$WrPnHO>g zg1@A%DvIWPDtXzll39kY6#%j;aN7grYJP9AlJgs3FnC?crv$wC7S4_Z?<_s0j;MmE z75yQGul2=bY%`l__1X3jxju2$Ws%hNv75ywfAqjgFO7wFsFDOW^)q2%VIF~WhwEW0 z45z^+r+}sJ{q+>X-w(}OiD(!*&cy4X&yM`!L0Fe+_RUfs@=J{AH#K~gArqT=#DcGE z!FwY(h&+&811rVCVoOuK)Z<-$EX zp`TzcUQC256@YWZ*GkE@P_et4D@qpM92fWA6c$MV=^qTu7&g)U?O~-fUR&xFqNiY1 zRd=|zUs_rmFZhKI|H}dcKhy%Okl(#y#QuMi81zsY56Y@757xBQqDNkd+XhLQhp2BB zBF^aJ__D676wLu|yYo6jNJNw^B+Ce;DYK!f$!dNs1*?D^97u^jKS++7S z5qE%zG#HY-SMUn^_yru=T6v`)CM%K<>_Z>tPe|js`c<|y7?qol&)C=>uLWkg5 zmzNcSAG_sL)E9or;i+O}tY^70@h7+=bG1;YDlX{<4zF_?{)K5B&?^tKZ6<$SD%@>F zY0cl2H7)%zKeDX%Eo7`ky^mzS)s;842cP{_;dzFuyd~Npb4u!bwkkhf8-^C2e3`q8>MuPhgiv0VxHxvrN9_`rJv&GX0fWz-L-Jg^B zrTsm>)-~j0F1sV=^V?UUi{L2cp%YwpvHwwLaSsCIrGI#({{QfbgDxMqR1Z0TcrO*~ z;`z(A$}o+TN+QHHSvsC2`@?YICZ>s8&hY;SmOyF0PKaZIauCMS*cOpAMn@6@g@rZ+ z+GT--(uT6#mL8^*mMf7BE`(AVj?zLY-2$aI%TjtREu}5AWdGlcWLvfz(%wn72tGczwUOgGD3RXpWs%onuMxs9!*D^698AupW z9qTDQu4`!>n|)e35b4t+d(+uOx+>VC#nXCiRex_Fq4fu1f`;C`>g;IuS%6KgEa3NK z<8dsc`?SDP0g~*EC3QU&OZH-QpPowNEUd4rJF9MGAgb@H`mjRGq;?wFRDVQY7mMpm z3yoB7eQ!#O#`XIBDXqU>Pt~tCe{Q#awQI4YOm?Q3muUO6`nZ4^zi5|(wb9R)oyarG?mI|I@A0U!+**&lW7_bYKF2biJ4BDbi~*$h?kQ`rCC(LG-oO(nPxMU zfo#Z#n8t)+3Ph87roL-y2!!U4SEWNCIM16i~-&+f55;kxC2bL$FE@jH{5p$Z8gxOiP%Y`hTTa_!v{AKQz&- ztE+dosg?pN)leO5WpNTS>IKdEEn21zMm&?r28Q52{$e2tGL44^Ys=^?m6p=kOy!gJ zWm*oFGKS@mqj~{|SONA*T2)3XC|J--en+NrnPlNhAmXMqmiXs^*154{EVE{Uc%xqF zrbcQ~sezg;wQkW;dVezGrdC0qf!0|>JG6xErVZ8_?B(25cZrr-sL&=jKwW>zKyYMY zdRn1&@Rid0oIhoRl)+X4)b&e?HUVlOtk^(xldhvgf^7r+@TXa!2`LC9AsB@wEO&eU2mN) z(2^JsyA6qfeOf%LSJx?Y8BU1m=}0P;*H3vVXSjksEcm>#5Xa`}jj5D2fEfH2Xje-M zUYHgYX}1u_p<|fIC+pI5g6KGn%JeZPZ-0!!1})tOab>y=S>3W~x@o{- z6^;@rhHTgRaoor06T(UUbrK4+@5bO?r=!vckDD+nwK+>2{{|{u4N@g}r(r z#3beB`G2`XrO(iR6q2H8yS9v;(z-=*`%fk%CVpj%l#pt?g4*)yP|xS-&NBKOeW5_5 zXkVr;A)BGS=+F;j%O|69F0Lne?{U*t=^g?1HKy7R)R*<>%xD>K zelPqrp$&BF_?^mZ&U<*tWDIuhrw3HJj~--_0)GL8jxYs2@VLev2$;`DG7X6UI9Z)P zq|z`w46OtLJ1=V3U8B%9@FSsRP+Ze)dQ@;zLq|~>(%J5G-n}dRZ6&kyH|cQ!{Vil( zBUvQvj*~0_A1JCtaGZW|?6>KdP}!4A%l>(MnVv>A%d;!|qA>*t&-9-JFU4GZhn`jG z8GrgNsQJ%JSLgNFP`5;(=b+M9GO8cg+ygIz^4i?=eR@IY>IcG?+on?I4+Y47p-DB8 zjrlar)KtoI{#kBcqL&4?ub@Df+zMt*USCD_T8O$J$~oMrC6*TP7j@H5trGV$r0P6I zV7EZ{MWH`5`DrX*wx&`d;C`jjYoc_PMSqNB290QXlRn_4*F{5hBmEE4DHBC$%EsbR zQGb7p;)4MAjY@Bd*2F3L?<8typrrUykb$JXr#}c1|BL*QF|18D{ZTYBZ_=M&Ec6IS ziv{(%>CbeR(9Aog)}hA!xSm1p@K?*ce*-6R%odqGGk?I4@6q3dmHq)4jbw+B?|%#2 zbX;ioJ_tcGO*#d0v?il&mPAi+AKQvsQnPf*?8tX6qfOPsf-ttT+RZX6Dm&RF6beP3 zdotcJDI1Kn7wkq=;Au=BIyoGfXCNVjCKTj+fxU@mxp*d*7aHec0GTUPt`xbN8x%fe zikv87g)u~0cpQaf zd<7Mi9GR0B@*S&l&9pCl-HEaNX?ZY8MoXaYHGDf}733;(88<{E%)< z^k)X#To3=_O2$lKPsc9P-MkDAhJ~{x<=xTJw2aRY5SSZIA6Gij5cFzsGk@S)4@C65 zwN^6CwOI9`5c(3?cqRrH_gSq+ox(wtSBZc-Jr5N%^t3N&WB|TT_i4!i3lxwI=*p)Y zn7fb%HlXhf8OGjhzswj!=Crh~YwQYb+p~UaV@s%YPgiH_);$|Gx3{{v5v?7s<)+cb zxlT0Bb!OwtE!K>gx6c4v^M9mL0F=It*NfQL0J0O$RCpt746=H1pPNG#AZC|Y`SZt( zG`yKMBPV_0I|S?}?$t7GU%;*_39bCGO*x3+R|<=9WNe!8jH- zw5ZJS(k@wws?6w1rejjyZ>08aizReJBo%IRb3b3|VuR6Uo&sL?L5j(isqs%CYe@@b zIID7kF*hyqmy+7D(SPa^xNVm54hVF3{;4I9+mh)F22+_YFP>ux`{F)8l;uRX>1-cH zXqPnGsFRr|UZwJtjG=1x2^l_tF-mS0@sdC38kMi$kDw8W#zceJowZuV=@agQ_#l5w znB`g+sb1mhkrXh$X4y(<-CntwmVwah5#oA_p-U<_5$ zGDc%(b6Z=!QQ%w6YZS&HWovIaN8wMw1B-9N+Vyl=>(yIgy}BrAhpc2}8YL-i*_KY7 ztV+`WKcC?{RKA@t3pu*BtqZJFSd2d)+cc07-Z#4x&7Dnd{yg6)lz@`z%=Sl-`9Z~*io zck_Lshk9JRJs=t>1jmKB~>`6+(J z@(S}J2Q{Q{a-ASTnIViecW(FIagWQ%G41y?zS)gpooM z@c<2$7TykMs4LH*UUYfts(!Ncn`?eZl}f zg)wx@0N0J(X(OJ^=$2()HLn)=Cn~=zx(_9(B@L04%{F_Zn}5!~5Ec5D4ibN6G_AD} zzxY^T_JF##qM8~B%aZ1OC}X^kQu`JDwaRaZnt!YcRrP7fq>eIihJW1UY{Xhkn>NdX zKy|<6-wD*;GtE08sLYryW<-e)?7k;;B>e$u?v!QhU9jPK6*Y$o8{Tl`N`+QvG ze}71rVC)fis9TZ<>EJ2JR`80F^2rkB7dihm$1Ta2bR?&wz>e`)w<4)1{3SfS$uKfV z3R=JT!eY+i7+IIfl3SIgiR|KvBWH*s;OEuF5tq~wLOB^xP_Dc7-BbNjpC|dHYJrZCWj-ucmv4;YS~eN!LvwER`NCd`R4Xh5%zP$V^nU>j zdOkNvbyB_117;mhiTiL_TBcy&Grvl->zO_SlCCX5dFLd`q7x-lBj*&ykj^ zR3@z`y0<8XlBHEhlCk7IV=ofWsuF|d)ECS}qnWf?I#-o~5=JFQM8u+7I!^>dg|wEb zbu4wp#rHGayeYTT>MN+(x3O`nFMpOSERQdpzQv2ui|Z5#Qd zB(+GbXda|>CW55ky@mG13K0wfXAm8yoek3MJG!Hujn$5)Q(6wWb-l4ogu?jj2Q|srw?r z-TG0$OfmDx%(qcX`Fc`D!WS{3dN*V%SZas3$vFXQy98^y3oT~8Yv>$EX0!uiRae?m z_}pvK=rBy5Z_#_!8QEmix_@_*w8E8(2{R5kf^056;GzbLOPr2uqFYaG6Fkrv($n_51%7~QN<>9$WdjE=H}>(a41KM%d2x#e@K3{W|+=-h*mR&2C01e z2sMP;YjU)9h+1kxOKJ+g*W=&D@=$q4jF%@HyRtCwOmEmpS|Rr9V_2br*NOd^ z4LN#oxd5yL=#MPWN{9Vo^X-Wo{a7IF2hvYWB%eUCkAZq+=NQ=iLI9?~@ zr+|ky4Rgm7yEDuc2dIe941~qc8V_$7;?7|XLk6+nbrh}e&Tt20EWZ@dRFDoYbwhkn zjJ$th974Z0F${3wtVLk_Ty;*J-Pi zP0IwrAT!Lj34GcoSB8g?IKPt%!iLD-$s+f_eZg@9q!2Si?`F#fUqY`!{bM0O7V^G%VB|A zyMM>SKNg|KKP}+>>?n6|5MlPK3Vto&;nxppD;yk@z4DXPm0z9hxb+U&Fv4$y&G>q= z799L0$A2&#>CfSgCuu$+9W>s<-&yq3!C{F9N!{d?I|g|+Qd9@*d;GplgY5Fk$LOV+ zoMealKns!!80PWsJ%(}L61B!7l?j1_5P#LRrVv%NBhs{R`;aufHYb&b+mF%A+DGl5 zBemAHtbLFi++KT(wv9*?;awp>ROX~P?e<4#Uf5RKIV{c3NxmUz!LYO#Cxdz*CoRQp zSvX|#NN06=q_eTU5-T!RmUJ?Ht=XQF8t)f+GnY5nY5>-}WLR1+R5pou?l@Y|F@KEX zk=jh-yq=Rn9;riE*;Slo}PfNKhXO#;FrZCf%VZ9h7W z<63YWE^s_SlAVQh6B(En9i<9%4AT|2bTQ4Ph2)pI?f2S`$j?bp`>_3(`Fz&?ig-FJ zoO7KAh@4BDOU>sBXV84Eajr9;>wlbW&OSUt&dug?oAV;`+3oBzpI18%%1wA4blzmb z-{QPYJmn_2-F$A5JI!a8+-p8Bk*^U?^f5j7uZ}jEz0E3;XbahB2iZwS&l4jj4WRS6 z3O&!w=ymQSl~7LUE99noXd2y1)9E>yK`+ouR%sTOQ@Qjt@<;lErGLk1wrw7r zV)M})+amJXs_9hQa++&vrqgU&Xr8T)=G&5Vy6vOnvt37L*nU7&ws&ZO-9`)TGA**t zpby#0X|df;etRud+s~#Y_7zlPZ=_oLg%q&wraF6s>g@;VO#2sUseO=^+3%&Z?61(- z_IKzU`+Kw;Blil&LR#qv&{rzQnG|%i(Q3zLI@gh)2FE^H;~1dx9G|AOj(e%mSwT(C z71Zp!jar*i3S|_ik_3{n0L4KavYWWZ2x3MhyU!66E$h=L+A&-s$9X_w9Q_e;+`-{ZW# z^Zn2H_I~`}!vGeFRRY^DyKK#pORBr{&?X}ut`1a(x__(dt3y_-*Np0pX~q39D{Rns z!iXBWZO~+oZu>($Mrf0rjM>$JZar!n_0_!*e@yT7n=HfVT6#jbYZ0wYEXnTgPDZ0N zVE5?$1-v94G2@1jFyj##-E1Um(naG-8WuGy@rRAg)t9Oe0$RJ3OoWV8X4DXvW+ftx zk%S(O8h?#_3B9-1NHn&@ZAXtr=PXcAATV*GzFBXK>hVb9*`iMM-zvA6RwMH#2^901uxUFh&4fT% zmP?pjNsiRIMD)<6xZyOeThl_DN_ZJ*?KUIHgnx{vz`WKxj&!7HbM8{w?{Rued(M1v zKHsK{_q=YI88@Bf0*RW@cIV@=<{eGsG21xrTrWycT7*KBd!eD2zb1R(O@H~k7>Duv zHPwp=n8;t#1>7~fuM9IaD5w%BpwLtNCe_Sq9eal4oj2DB1#<+(MGR-P&Ig%3t%=!< zS$|KxI1a~an2Q>L$s;1$9nQJal4dk)Box$YsAKgCiEGni##jr|%So6Y4J@pYBF!;~ zhXwpKhc7&QZ$=e~Sb&ABZ4o)&U~N*dSU`2G^eQh-WCe9tA}~Ae369btLlB{GjOKB@yEDH!C7Q&df^#X zi~?{rCuAE|kAjKzt+r#t6s)1h840@A<%i5(O;$Q&tD(opg0)yzgm#=ucf4CSqkqYS zaTdivk5I~#=1Z9K5M*uV6H??6s9*ynT`vzr2@%Tkr4k+Tr_ib40$fPP7$yLA$cwJ@ zF@`94=op)$x^0t+QAsNY$pi!4e7hp~gO=|yD=^8JTvTiC(HAamYEQ}t z+hR~QoKTOz%)IHEg&6iC4vP=3mw&u4wvcSwi$vNBGQE5RoSUs^l+u{A+6s~aMMkXG z+1g4wD8^Y27Oe4f``K{+tm76n(*d6BUA4;pLa26`6RD6?Rq?2K1yMXVAk`&xbks*~{+``Mhg4cQEuw+aM zaI9{}9en8DCh*S9CojIk)qh|k?#iNiCQ}rAmr&iYRJiND ztt+j*c+}Fv&6x&7U~!(Sb1eAz1N@Nf`w?YxGJdhy+seiNNZEYIG1_<^?&pm^P8W?d ze(p@$nWC`Pxqpf8d&AIGNJn#Ty)j z1NbA^Y}pNQ>OfTdiAp+WR>C6390IrFj;YZglitGH8r7(GvVRpWjZd7|r24M{u66B) zs#VS$?R*!1FT&sO-ssvW8s5jh$-O=^9=7^y z75||~QA6zLW}Lu!YOZh1J$j46m zNH|;^a$U_RKgla5h>5(igl^ek(~2nL5a_0}ipvA_Xf0k*E-ExJNld0{LZ;F^DzqAL+IZGJ7<3i1szf zxMRkQ(|@;wj9%I7h{c*{;?g%giylU}Dz{iwb(1vGK<-vlnKs!|Mb9}iTt)Rl&NZka zkkugrMiY(ng3QseY!npaOf1jo3|r35nK+eTYh*`DHabuv@IFy zG7@V!LWE0&)bvqgQ8=-L-(vt#Z-&xaOj3G@Nqw1FfbNQ`!bFEl@z)0)+#Z5e#_hQ|Rd!KrEoRn^aFz zkzYzz%hher>ixcg6fW`=rr>Nx@enQ!sQqYR{<2^|eUfw?e8;B_`T)Kxkp8${U>g?k*VhCd zp^yYLvi}<#5TDjrx@{0U$jx*tQn+mhcXsq2e46a@44^-Sd;C6S2=}sK1LQ_OUhgO` z^4yN+e9Dv9TQ64y1Bw)0i4u)98(^+@R~eUUsG!Ye84 zFa7-?x3cqUXX)$G<2MgYiGWhjq?Q-CE(|sm-68_z>h_O2vME5nX;RodIf)=No(={I z_<&3QJcPg8kAI}_Vd+OH4z{NsFMmjv3;kunMSh94VNnqD?85uOps%nq=q?kU_JT5@ zwih;eQlhxr)7d^K#-~InWlc&<*#?{A(8f^+C_WmRR{B&Yh3pxhLU9-toLz%rCPi}} zE!cw^pQlXB3aACUpacU&ZlBUl(Jo4fxpbDVwDn^m{VG||ar9B)9}@K`(SJxmAWro& z_3yzfUqLoXg`H($!I;FTudPdo6FTJm2@^S|&42H(XbSRW7!)V&=I`{;mWicu@BT7z zQs!)F9t-K|aFaMsoJ_6z-ICrzjW5#yJRs>~)bugki)ST$8T%!D4F@EBliCNSA5!fl zN;OuKbR3m0rj=rrq}5`nq<<%iHIl|euXt6QA}$hFNqV)oR?_Rm4oPnoLy|ru_DQ-= zJTDFa;zjY2p{sg zWqz0I5y>-U{xR1Rl4r{NQ?6Ge&y@N7t~Vsll=-(^?@FF2^Y6JnkbgW==09{7N}eh4 z?h`%x-LM8D}+*41ZA#EG0D9KQjc2#z59Pq zO9u!y^MeiK3jhHB6_epc9Fs0q7m}w4lLmSnf6Gb(F%*XXShZTmYQ1gTje=G?4qg`Z zf*U~;6hT37na-R}qnQiIv@S#+#J6xEf(swOhZ4_JMMMtdob%^9e?s#9@%jc}19Jk8 z4-eKFdIEVQN4T|=j2t&EtMI{9_E$cx)DHN2-1mG28IEdMq557#dRO3U?22M($g zlriC81f!!ELd`)1V?{MBFnGYPgmrGp{4)cn6%<#sg5fMU9E|fi%iTOm9KgiN)zu3o zSD!J}c*e{V&__#si_#}hO9u$51d|3zY5@QM=aUgu9h0?tFMkPm8^?8iLjVN0f)0|R zWazNhlxTrCNF5d_LAD%TwkbkKL>+-8TV4VSawTAw*fNnD^2giQT{goNRR~OwAH5%vorH%=FNNm``;VB z_N`CeB%?_hv?RK-S(>S)VQBau{&NwD>j_ zF-Hwk*KNZb#pqexc5oKPcXjOO*cH#{XIq~NkPxH{TYm*Rtv_hwbV2JZd$e=Z)-pN0 z^PH`XkLz~lpy{|;F6Sq&pjD@}vs!0PGe z6v$ZT%$%iV1Z}J(*k7K8=sNv;I#+Ovvr?~~bXs?u{hF!CQ|_-`Y?!WYn_8|j3&GBu zl|F+DcYh8nxg49<-)ESHyI0Vo;oInYTMcVX9@5;g9>>x1BRMQ@KPJc%Za)^J6|_nr zKQ#*4^Z(G>Pt6Lgrp6!zX?X+rXibm;)WBbN1WBP~{Iw45)a0toTeof%G+Oh5Wryxb zN@p5YCm&YsN!Jd$jG8^|w^_Wo-1ad{*|(#*+kcnS97j-dxV>sGIk+cCchX&K1yxY6 z`dB};!Xf&3!*LyHut$Qlnc5WEME3}4k)j3H$aVHvxg78Y3_E@b3u@5wjX7b zPLz^7h65uMRj8d}5Y1tP55ozK;r0{r?;WHL>g4laujaX3dTd*h+xuy|LOa-f%M7RA zuz#V1WlscYXGzO0Xsu-c>6UPEVQ}o>+w7v~meKw6 zfS|`8k|tL(5VDPt0$*C)(&lVYGnVeCrsb+>%XBrvR5fz~VkMmn-RV#V&X1#`XH?fx zvxb>b_48WV%}uD=X5}V20@O1vluQ2hQ-2>^k+tl+2Al20(<||vxfpIJ~|9`dJ zVH^pxv&RS97h5DqN9ZW4!UT{rMgsH>#tHOouVIW{%W|QnHohN<4ZE5RR@l7FPk$#A zI?0%8pKlXW%QH2&OfWTY{1~5fO3=QyMi3vb*?iSmEU7hC;l7%nHAo*ucA`RmedXLF zXlD(SytNYn`{9Rs;@fw21qcpYFGUH*Xmdk{4fK z0AKh-FGJC#f0Ik!{d{T7B7elr2J8>e z4=VKi^h2D=Q8&0_LHc1j$T9pQ7-FcHxZj3w-{RF}MXBm@?_X&zG?V%-Bet=g# zgEZn=6W?w3jeoQ(!&ECWHqJ zs;lJ@+Tf9MhC9~LX7*WT*0A%cJEpn#(bX;0i-*TF1j2A3zeOFlEi7~=R7B$hpH(7@ zc$q9Z%JU#Am8%BTa1gvUGZPX)hL@#()Y8UP?D?tiCHan51waKUtqypCE-ALn&``k4jkeO@}6ROkhI5oJaRd?*oW z5XmD5>YOZAT4pPd`M`dOKE|;8c#wXMeqKQ__X$u$!F<91^W0T4GtRNpyh;fxIv+8{ zOV!mig|0Jq`E}FfEGH;5uUHx|3whm^-h~cRG|loa&)cs`#D7mW5K(xZ?6+)vAgAZC zD+2J-T)KRUZh~%1{k&VASQx^y`SF+OS6KX4kyjRJJpeT){PgS47=e2L=`KjGaKL_s zUIno%SwM4WAF(xl=4hpof(h_9QEfU}Rt7%rCFq{-h?=0}Z_#HJdX0XYPezSbpFe{d z0C)YJ60>{(bbnZJLT@3P<#<0>aI5md?+Lo2+D-Fke_x?5v0p-So~;%rL+cL|`Xc=y zDo2?BXJ-XJpB{>GjhRUa08Q0fc~|Te5H?$jM>&XZG_?d?@$c3DX04&{U<}^Kj^=z zll8%>K>i=dqr$~=S9jB6O9hsxyPZc556Zw=j_nVDRZX|_LS7YaUr=}9egcpXb&Lyu z)YmbNGJh^0d;nj66%_}BAGOYHUX^~)0N68LkJ^TyJHrdKncoeHWg@5uMJ!*CaF?vi zs}inQ2`7nFmB(0lPrqn_`mS~KaI)&6rO6}?TrFA@(Ja=?UzYTXI{;CnCeCzb>5&FP zU9f&`4m+(A>lG0a8$bbgJoRdhk?tvg@Ikz#RDUy9`Bv_`)Mkhjai_S8ErG{n6Y!ZX zjPs#^rE8v{eXb(WZW}1zS0~dl)qaDzZc6#Eb{ck_GRA z#30&5L=j;Tg=w(=Im_LHt$@}KL1QA*~192~ak5Zap zUm99S=A}`1@@=9=5f6x7EHE6dJZ-x$j_M#N`oWZ#8SoMRTSbJEkaI_E1S`LPb#u`l za~4L#=6*e^6>@H+e`vvSoIfb`u^orz|9^Gmf4h-i>_^V46i#@Dxdo?h3>Vd9UB7Q1 zd*h%uq=*CJ?O?Lm(&(J#sK(r_I|5=@p*QJ8=tPJL3W(!iGFv{}j#xpF;@rMTpd4td z<_1}s1;k09u3T^?RJY`6H5?F+aq(TFbgz!+$2p?$R`cYY_JBwWirgNmvn*Q5HGe{f z-XaT1oDGR#3t6;+$vF}g;7xCzl>r&9Od6(sppYNY?IXMuZ9`V@!`mKeeSE_wM4Gd+URu(#jex(s}ep9w1GC3 z7Kw+jq#o_EXrxGYA1~6D%cM+Ge1B+?9*7ocTWaW4s-L{|jmQn!kxEX{y*KxIy1Xsk zjnC7@NQ-xSD&Z?q_a#!IA$;sPe$gu?Z@nHJio8s36Lg7G@2AP18uG-3n|dSD^zhIP z+Lua-$Q13Lqz^#~2=HF178_n9HXiZ3Ovmd`>ukdKrc^2!X-ZAeBT)7dg@2>+{JWz! z=p-xnDEg15lCRLp=uPi))DZP-pCqq%wfcyWMMo@`orpju`U#jwh%@+&z~1$+@gb_i z)6qj`VXXJU%FkkS64rkme)%TMc?)t4l%`DCsP&j<&wVcTDtWIqWv3~3;0Bqggf}`x z?`&K}p9&;=Aun6(T&k=7S$}GZhkTxv`XW6!32V~_TI%bru-U&74|$7pp-A6@^%t>z zik|j#`C5GOo6l26yv4Vpk#1d>ruU>0Sp1{7@3N40)z%`t|2VeC&_KN}@=GU4?^hP}~YUu?KOKHT)vA#ce-FMp(9pP!wPTFk%# zEwqky;$|C=p1Ezu@6K6!t$>6N_Ie-e^%}k#xcn}ovllZSv|SPDuQ-}tU^i{{+`l1; z+iYOZMxq` zyNmevH37(cCUt;!hJWefMf#0t`kVyL=P%JpzSQp?pS<i{A@amJ0F;?aT#H3gGL(m+ zMd2x(2y7PxEPwgIW>H_-O1kRG@$x~jQ_UiPlcvRrqG+t>u>Js>8_Xp<>`syJiiA&! ztVK|;R}+4AD**Ck_Nds%Xh&S}{}jiCxVtDeH;a2t6-Dft*jg0#%HQsyNF;oXVK{$( zQQY6LPpMO5t9niY*so`U_cqrfS%ttA> zMrrXr{mf-r8(+hNdUxQONMdM>QWS?n{+OpF2q5te-AZ?0^44=hA%DU`#Rc;$`A425WvPKyy?$o4V#Hc#hepIh#q zrzgc`^ts)D{=4V}+2@w~FVe?kpIh#KoUY0~x7_FGtMoP5=a&0# zq5$MRx9AIxXym?ZxgQhVvd=B|)8ZMaXDKe4fFb_31FMfwok)^Lq|q0WrRvD@ZBR=G z2pQ0I&-V@h0C*ge;YJ*jtBNjvYflqF6o%gs=t3z%xd|2&*IQdyR=^LH8WYpRgrrep z4Mx6Aw}fxhSE$jN_`x6Gk20R2MM&C)-R$h{nfE#GnVgwFe}DZ3unAM( z^yK7C>62cU)*<-~eOtHo^)=lJyq4q2*a>{Y3mU}nkX(`x@nlm*hSem0>o7{ZNZ;O< zZbWN(%QigOG8~nI>Q5dw>RYT0OXvK4;<_A&n$p-%65n=wqR{bejviAOu@}cn>s#w3 zqd~{|=TQiObS+3ii(WV`2`mPoZQ7x1xMY3^WvfM@Sq*HPLJh+LQwQ=`ny&P1^Hu$T ztXM-zVD=*VoC&`n>n>@37!?>fN*sy>#GXLvspC8GGlAj!USU^YC|}skAcN~^Xqe0( zjqx#zAj>muU<=IUs~34|v06u2ahGbSeT-uAG|Vv*Bw$#pf8#qXFt zMfw|VuC{UeT)2WpJ6&O+E6jF;;~n9>cf~Ip6j-_@&PGFD0%Vu*QJ@Ht`C7Og!xt#L> zmqlJGEh<%*ATJUmZc(FfNSB##fy_`Y-70r{Iv3jEfR|~Ii!xC44vZ(KNj#>kjsE86 zE3FB*OayD~$|}3Y&(h6^X|1 z(TcJ}8{Ua3yL1loSfg!2gTekntVO7WNyFQCfwF2ti$UvL8C6{{IPBg01XK~$ThIQx z{)~aw>(9F2L#G36*kRDPqA$P*nq=!@bbQ#RzDpVIfYc*x9=}2N^*2z1E%3epP)i30 z>M4^xlbnuWe_MAGRTTb?O*?TCw6v5$6bS)qZqo=w4J~*9i;eVx4NwO!crrOjhE8U( z&P-ZZU9$We^ubqNd73QDTJqqV55D;u{1?`JQre~$mu9WZ%=z|x?{A;q|NiAy0GH5U z*nIM2xww(4aBEe#)zoy#s-^NN%WJl5hX=Oj8cnY%e+ZYt5!@FfY;fPO8p2xj+f6?; zUE_`~@~KwcX!4d}D<7hA<#M$$MY^)MV_$1K4gr3H8yA&|Ten>yr0v!TT@%u$ScDfR zrzVR=Rjj3cjDj)fWv?wQanp7LL)Me^LS6EzBMR%1w^~9L%8&g(G;d3f4uLKFIqs5J zYKSlle?R1Fyx?%RURbI;6jq>Nh+(uYf`e8J=hO2&ZQCoTU^AKRV>_^&!W{P-3%oVM zaQqOcL1!4cYP)vuF~dMQb1#lKj_HWu4TgBXPYuJQYWv&8km~(7Mlh=5I8HE}*mJ#? zmxhx%#+9e>eorO0)eg#m6uhb7G^KSg`Cbxlf9XizZH9>B@hZcqJ*7VTp6)w1tHLB1 z1}(?)MI0$rLIUS0;Z^atECLmzzb6FE#PKdBl;L{}$M%UdWEi4$AS4ew$#8O?ZRr(G z4syuHkcGi8a#*gRz@QP|7R93=j*A$L;eA}9id+JyWjkK`Mod00;{&DlA!QJFR3&lj zf1vI*O1ec{(V=0QA?ELLVls-W``ELsu7M`3`vI4MzhVcpJ!9#^KGjq|#b-J`!F7h$ z{dUEFmBLuMbYu>nV^(S3q+UC;7s@e_qZG#+N=oo0o$G1>6Y0a{9@&9;EU2+8k|7P6 zp?HMh|8#X5UnwpxGbHw;%WXHXn_~8nedvw09V+G$(lhoq7L}=qb+OaPSD&;$TuUtG(4;py( zh)8|Nord(*d1ZH-Dmw1MqU&RKiI)26r-hE(pqnmo4uixe^`qea7(_HA_R2KjdJ4$g!)7ve&Q^b1Tf+{(Vd6vInCd>i725IomG^(Ez(D8L!4qlUAX=)EV9!3JfWLB4n1z)!ums&0UuuVLUH zP)i30*5f6tnvk?lbhL{|8I78X7|_cA3p(L9<~X5y1L3{K8Sf*xL|5gToDT;aYig?m8z^z zQ`XdEMJqC#*O|ho!7x~+MzT<5g$turF~pS;RSY&GR;6TxR)3Q+&%yG`3&ngIwR*qK&t{TERu@0|fDrKKw3=RE&t-)Xh-$i& zl5|>BSn5)z)hg3d?<~8msU=ye>CHWR!9yT;PU|$KP*qADf(V?zj^n^g~nykv^I)Uz3{78Ty81{n~ zZsS&7WH)#Ach3%UyVD1s=Ahvw9*%Wt z<42vTt%|niux3Zww13+oK)-d~G>VKHM0ov>KXKaUH(Cc)#9GFVSc4EoUbnRudxi}T z8J!VNY=4g*Y7C*Ho7#^wUVt&67&ea4^1oBw%@h^ z+YZ+eK^VI5573*KZosq?pMj(u5257?^lBu&LF9`ao`sYf9&zx;uK2iv&$;8{ z4nFUSFF5$3JHFuHORo5YgFkV{CmcNEicdQDvO7NM;484|f=_+6!)x%g1CL;L9DE%% zT=1xaKZ8v-+-@x1OZ;|0_a9J82MFd71j+6K002-1li@}jlN6Rde_awnSQ^R>8l%uQ zO&WF!6qOdxN;eu7Q-nHAUeckHnK(0P3kdECiu+2%6$MdLP?%OK@`LB_gMXCA`(~0R zX;Tm9uJ&d7>n z%9A~GP*{Z zrpyh7B^|a-)|8b<&(!>OhWQ08$LV}WQ`RD4Od8d3O-;%vhK7#W<7u;XvbxQo0JX@f zY(C0RS6^zcd>jo287k@<4tg;k3q5e5hLHE@&4ooC)S|`w7N|jm>3tns$G}U4o!(2g=!}xLHp?+qF zvj$ztd<%96=4tCKGG@ADSX{=mNZ@ho6rr?EOQ1(G2i@2;GXb&S#U3YtCuVwc*4rJc zPm$kZf2+|!X~X6%(QMj{4u)mZOi!(P(dF3hX4ra9l=RKQ$v(kJFS#;ib+z9K^#Gle z6LKa>&4oMFJ4C&NBJ7hhPSIjcOno$M6iq+l;ExpH9rF68@D3-EgCCf}JJSgVPbI1$ z?JjPPX!_88InA}KX&=#cFH#s3Ix<6LeY==wf5DK*jP`hqF%u+|sI)3HfyywfAj=0O zMNUX2pLR;T(8c+$g&}Z#q9L>(D~t~l&X^VFXp@&w92f8tq+KXMZ&o!an%$#uo^hJh z^9-RjEvqE_s%H8{qw(juo4?SC{YhO*`|H*ibxm%ZF6r=2QC)bE`d3oZ(~?;a-(mX)b!|i%p!VVP>DN6tg*Ry97gUPUJj<}OxaYL1nXE}h zxs-O{twImUw z43Eo6nJ4_RTDIQALB8H!3nq37cE6>oNG;jZZhXh!vORPsMKfzJ8_*?O7DfGmcrL8A z(_NAhSH+JE?u?`xR1|ZThDb;2Dt`9hC;UQ%94^20-MA*;<$KO0{3b&9y(ENIe@&xj z6>X23)Ftc?ax=4pL5FZ06CPOjgG%2*lbx;+sVm6EHifaku2RZ6dm2zO1s^4+O| zX?^Rl!e{47y>uJGVh+yEaNe$4U2tTYyJ3nqt9nkQP8+X`9>;yxHT1=;SB4=QU*?nq zndTZfT|OzWa_zE$8FPQtuK2+Z>H-NyCcc=wWX>wq$q7{vij#xqCQBclE;KU_SpRHh zW?)cb0G=uW2QHH@&UKOjUxp5p-v+$&z!*iIUwCrEeC5gh!qSr;%oC7--UiJO%g(@H zgQD=VC|Kd1c_uQ*S7+LyC@PW!E7G5DDhEzd%(QbXn4J;PQoYKo1+C zI4^v%{X#z$(3LimCoU9YO4kMJJG0PS25}<7q9LXMM{Esm6)13%7{fk7Wdx5wm$C1R5emYB+b4!_g{ zCYC2a7ogf;<2t!#hh+G05lGD55CT^#LlBoxIEo9C9q6 zV^AjZEfZsU6$%s=ojiXT+hlLxY4o6EhgiZ7JP-%P5cLSCVgnh(`W^-bB@{)=b3uwG zE!U6%u3dpFT>%EaE{d8bl@K+c6+w`+ju^dTU{F9&yQvzYmVNS(GoZm{D-R;bE=#wApMmV(yJpr(t7y*s2{B8_zE)_ yL|YQw3&NAZiu6_*%Ye#&V4x{Sc^DWpP)tgl235p9dFD!GE+Jk92JyL|;s5}0b2K*q delta 34555 zcmX7vV`H6d(}mmEwr$(CZQE$vU^m*aZQE(=WXEZ2+l}qF_w)XN>&rEBu9;)4>0JOD zo(HR^Mh47P)@z^^pH!4#b(O8!;$>N+S+v5K5f8RrQ+Qv0_oH#e!pI2>yt4ij>fI9l zW&-hsVAQg%dpn3NRy$kb_vbM2sr`>bZ48b35m{D=OqX;p8A${^Dp|W&J5mXvUl#_I zN!~GCBUzj~C%K?<7+UZ_q|L)EGG#_*2Zzko-&Kck)Qd2%CpS3{P1co1?$|Sj1?E;PO z7alI9$X(MDly9AIEZ-vDLhpAKd1x4U#w$OvBtaA{fW9)iD#|AkMrsSaNz(69;h1iM1#_ z?u?O_aKa>vk=j;AR&*V-p3SY`CI}Uo%eRO(Dr-Te<99WQhi>y&l%UiS%W2m(d#woD zW?alFl75!1NiUzVqgqY98fSQNjhX3uZ&orB08Y*DFD;sjIddWoJF;S_@{Lx#SQk+9 zvSQ-620z0D7cy8-u_7u?PqYt?R0m2k%PWj%V(L|MCO(@3%l&pzEy7ijNv(VXU9byn z@6=4zL|qk*7!@QWd9imT9i%y}1#6+%w=s%WmsHbw@{UVc^?nL*GsnACaLnTbr9A>B zK)H-$tB`>jt9LSwaY+4!F1q(YO!E7@?SX3X-Ug4r($QrmJnM8m#;#LN`kE>?<{vbCZbhKOrMpux zTU=02hy${;n&ikcP8PqufhT9nJU>s;dyl;&~|Cs+o{9pCu{cRF+0{iyuH~6=tIZXVd zR~pJBC3Hf-g%Y|bhTuGyd~3-sm}kaX5=T?p$V?48h4{h2;_u{b}8s~Jar{39PnL7DsXpxcX#3zx@f9K zkkrw9s2*>)&=fLY{=xeIYVICff2Id5cc*~l7ztSsU@xuXYdV1(lLGZ5)?mXyIDf1- zA7j3P{C5s?$Y-kg60&XML*y93zrir8CNq*EMx)Kw)XA(N({9t-XAdX;rjxk`OF%4-0x?ne@LlBQMJe5+$Ir{Oj`@#qe+_-z!g5qQ2SxKQy1ex_x^Huj%u+S@EfEPP-70KeL@7@PBfadCUBt%`huTknOCj{ z;v?wZ2&wsL@-iBa(iFd)7duJTY8z-q5^HR-R9d*ex2m^A-~uCvz9B-1C$2xXL#>ow z!O<5&jhbM&@m=l_aW3F>vjJyy27gY}!9PSU3kITbrbs#Gm0gD?~Tub8ZFFK$X?pdv-%EeopaGB#$rDQHELW!8bVt`%?&>0 zrZUQ0!yP(uzVK?jWJ8^n915hO$v1SLV_&$-2y(iDIg}GDFRo!JzQF#gJoWu^UW0#? z*OC-SPMEY!LYYLJM*(Qov{#-t!3Z!CfomqgzFJld>~CTFKGcr^sUai5s-y^vI5K={ z)cmQthQuKS07e8nLfaIYQ5f}PJQqcmokx?%yzFH*`%k}RyXCt1Chfv5KAeMWbq^2MNft;@`hMyhWg50(!jdAn;Jyx4Yt)^^DVCSu?xRu^$*&&=O6#JVShU_N3?D)|$5pyP8A!f)`| z>t0k&S66T*es5(_cs>0F=twYJUrQMqYa2HQvy)d+XW&rai?m;8nW9tL9Ivp9qi2-` zOQM<}D*g`28wJ54H~1U!+)vQh)(cpuf^&8uteU$G{9BUhOL| zBX{5E1**;hlc0ZAi(r@)IK{Y*ro_UL8Ztf8n{Xnwn=s=qH;fxkK+uL zY)0pvf6-iHfX+{F8&6LzG;&d%^5g`_&GEEx0GU=cJM*}RecV-AqHSK@{TMir1jaFf&R{@?|ieOUnmb?lQxCN!GnAqcii9$ z{a!Y{Vfz)xD!m2VfPH=`bk5m6dG{LfgtA4ITT?Sckn<92rt@pG+sk>3UhTQx9ywF3 z=%B0LZN<=6-B4+UbYWxfQUOe8cmEDY3QL$;mOw&X2;q9x9qNz3J97)3^jb zdlzkDYLKm^5?3IV>t3fdWwNpq3qY;hsj=pk9;P!wVmjP|6Dw^ez7_&DH9X33$T=Q{>Nl zv*a*QMM1-2XQ)O=3n@X+RO~S`N13QM81^ZzljPJIFBh%x<~No?@z_&LAl)ap!AflS zb{yFXU(Uw(dw%NR_l7%eN2VVX;^Ln{I1G+yPQr1AY+0MapBnJ3k1>Zdrw^3aUig*! z?xQe8C0LW;EDY(qe_P!Z#Q^jP3u$Z3hQpy^w7?jI;~XTz0ju$DQNc4LUyX}+S5zh> zGkB%~XU+L?3pw&j!i|x6C+RyP+_XYNm9`rtHpqxvoCdV_MXg847oHhYJqO+{t!xxdbsw4Ugn($Cwkm^+36&goy$vkaFs zrH6F29eMPXyoBha7X^b+N*a!>VZ<&Gf3eeE+Bgz7PB-6X7 z_%2M~{sTwC^iQVjH9#fVa3IO6E4b*S%M;#WhHa^L+=DP%arD_`eW5G0<9Tk=Ci?P@ z6tJXhej{ZWF=idj32x7dp{zmQY;;D2*11&-(~wifGXLmD6C-XR=K3c>S^_+x!3OuB z%D&!EOk;V4Sq6eQcE{UEDsPMtED*;qgcJU^UwLwjE-Ww54d73fQ`9Sv%^H>juEKmxN+*aD=0Q+ZFH1_J(*$~9&JyUJ6!>(Nj zi3Z6zWC%Yz0ZjX>thi~rH+lqv<9nkI3?Ghn7@!u3Ef){G(0Pvwnxc&(YeC=Kg2-7z zr>a^@b_QClXs?Obplq@Lq-l5>W);Y^JbCYk^n8G`8PzCH^rnY5Zk-AN6|7Pn=oF(H zxE#8LkI;;}K7I^UK55Z)c=zn7OX_XVgFlEGSO}~H^y|wd7piw*b1$kA!0*X*DQ~O` z*vFvc5Jy7(fFMRq>XA8Tq`E>EF35{?(_;yAdbO8rrmrlb&LceV%;U3haVV}Koh9C| zTZnR0a(*yN^Hp9u*h+eAdn)d}vPCo3k?GCz1w>OOeme(Mbo*A7)*nEmmUt?eN_vA; z=~2}K_}BtDXJM-y5fn^v>QQo+%*FdZQFNz^j&rYhmZHgDA-TH47#Wjn_@iH4?6R{J z%+C8LYIy>{3~A@|y4kN8YZZp72F8F@dOZWp>N0-DyVb4UQd_t^`P)zsCoygL_>>x| z2Hyu7;n(4G&?wCB4YVUIVg0K!CALjRsb}&4aLS|}0t`C}orYqhFe7N~h9XQ_bIW*f zGlDCIE`&wwyFX1U>}g#P0xRRn2q9%FPRfm{-M7;}6cS(V6;kn@6!$y06lO>8AE_!O z{|W{HEAbI0eD$z9tQvWth7y>qpTKQ0$EDsJkQxAaV2+gE28Al8W%t`Pbh zPl#%_S@a^6Y;lH6BfUfZNRKwS#x_keQ`;Rjg@qj zZRwQXZd-rWngbYC}r6X)VCJ-=D54A+81%(L*8?+&r7(wOxDSNn!t(U}!;5|sjq zc5yF5$V!;%C#T+T3*AD+A({T)#p$H_<$nDd#M)KOLbd*KoW~9E19BBd-UwBX1<0h9 z8lNI&7Z_r4bx;`%5&;ky+y7PD9F^;Qk{`J@z!jJKyJ|s@lY^y!r9p^75D)_TJ6S*T zLA7AA*m}Y|5~)-`cyB+lUE9CS_`iB;MM&0fX**f;$n($fQ1_Zo=u>|n~r$HvkOUK(gv_L&@DE0b4#ya{HN)8bNQMl9hCva zi~j0v&plRsp?_zR zA}uI4n;^_Ko5`N-HCw_1BMLd#OAmmIY#ol4M^UjLL-UAat+xA+zxrFqKc@V5Zqan_ z+LoVX-Ub2mT7Dk_ z<+_3?XWBEM84@J_F}FDe-hl@}x@v-s1AR{_YD!_fMgagH6s9uyi6pW3gdhauG>+H? zi<5^{dp*5-9v`|m*ceT&`Hqv77oBQ+Da!=?dDO&9jo;=JkzrQKx^o$RqAgzL{ zjK@n)JW~lzxB>(o(21ibI}i|r3e;17zTjdEl5c`Cn-KAlR7EPp84M@!8~CywES-`mxKJ@Dsf6B18_!XMIq$Q3rTDeIgJ3X zB1)voa#V{iY^ju>*Cdg&UCbx?d3UMArPRHZauE}c@Fdk;z85OcA&Th>ZN%}=VU%3b9={Q(@M4QaeuGE(BbZ{U z?WPDG+sjJSz1OYFpdImKYHUa@ELn%n&PR9&I7B$<-c3e|{tPH*u@hs)Ci>Z@5$M?lP(#d#QIz}~()P7mt`<2PT4oHH}R&#dIx4uq943D8gVbaa2&FygrSk3*whGr~Jn zR4QnS@83UZ_BUGw;?@T zo5jA#potERcBv+dd8V$xTh)COur`TQ^^Yb&cdBcesjHlA3O8SBeKrVj!-D3+_p6%P zP@e{|^-G-C(}g+=bAuAy8)wcS{$XB?I=|r=&=TvbqeyXiuG43RR>R72Ry7d6RS;n^ zO5J-QIc@)sz_l6%Lg5zA8cgNK^GK_b-Z+M{RLYk5=O|6c%!1u6YMm3jJg{TfS*L%2 zA<*7$@wgJ(M*gyTzz8+7{iRP_e~(CCbGB}FN-#`&1ntct@`5gB-u6oUp3#QDxyF8v zOjxr}pS{5RpK1l7+l(bC)0>M;%7L?@6t}S&a zx0gP8^sXi(g2_g8+8-1~hKO;9Nn%_S%9djd*;nCLadHpVx(S0tixw2{Q}vOPCWvZg zjYc6LQ~nIZ*b0m_uN~l{&2df2*ZmBU8dv`#o+^5p>D5l%9@(Y-g%`|$%nQ|SSRm0c zLZV)45DS8d#v(z6gj&6|ay@MP23leodS8-GWIMH8_YCScX#Xr)mbuvXqSHo*)cY9g z#Ea+NvHIA)@`L+)T|f$Etx;-vrE3;Gk^O@IN@1{lpg&XzU5Eh3!w;6l=Q$k|%7nj^ z|HGu}c59-Ilzu^w<93il$cRf@C(4Cr2S!!E&7#)GgUH@py?O;Vl&joXrep=2A|3Vn zH+e$Ctmdy3B^fh%12D$nQk^j|v=>_3JAdKPt2YVusbNW&CL?M*?`K1mK*!&-9Ecp~>V1w{EK(429OT>DJAV21fG z=XP=%m+0vV4LdIi#(~XpaUY$~fQ=xA#5?V%xGRr_|5WWV=uoG_Z&{fae)`2~u{6-p zG>E>8j({w7njU-5Lai|2HhDPntQ(X@yB z9l?NGoKB5N98fWrkdN3g8ox7Vic|gfTF~jIfXkm|9Yuu-p>v3d{5&hC+ZD%mh|_=* zD5v*u(SuLxzX~owH!mJQi%Z=ALvdjyt9U6baVY<88B>{HApAJ~>`buHVGQd%KUu(d z5#{NEKk6Vy08_8*E(?hqZe2L?P2$>!0~26N(rVzB9KbF&JQOIaU{SumX!TsYzR%wB z<5EgJXDJ=1L_SNCNZcBWBNeN+Y`)B%R(wEA?}Wi@mp(jcw9&^1EMSM58?68gwnXF` zzT0_7>)ep%6hid-*DZ42eU)tFcFz7@bo=<~CrLXpNDM}tv*-B(ZF`(9^RiM9W4xC%@ZHv=>w(&~$Wta%)Z;d!{J;e@z zX1Gkw^XrHOfYHR#hAU=G`v43E$Iq}*gwqm@-mPac0HOZ0 zVtfu7>CQYS_F@n6n#CGcC5R%4{+P4m7uVlg3axX}B(_kf((>W?EhIO&rQ{iUO$16X zv{Abj3ZApUrcar7Ck}B1%RvnR%uocMlKsRxV9Qqe^Y_5C$xQW@9QdCcF%W#!zj;!xWc+0#VQ*}u&rJ7)zc+{vpw+nV?{tdd&Xs`NV zKUp|dV98WbWl*_MoyzM0xv8tTNJChwifP!9WM^GD|Mkc75$F;j$K%Y8K@7?uJjq-w zz*|>EH5jH&oTKlIzueAN2926Uo1OryC|CmkyoQZABt#FtHz)QmQvSX35o`f z<^*5XXxexj+Q-a#2h4(?_*|!5Pjph@?Na8Z>K%AAjNr3T!7RN;7c)1SqAJfHY|xAV z1f;p%lSdE8I}E4~tRH(l*rK?OZ>mB4C{3e%E-bUng2ymerg8?M$rXC!D?3O}_mka? zm*Y~JMu+_F7O4T;#nFv)?Ru6 z92r|old*4ZB$*6M40B;V&2w->#>4DEu0;#vHSgXdEzm{+VS48 z7U1tVn#AnQ3z#gP26$!dmS5&JsXsrR>~rWA}%qd{92+j zu+wYAqrJYOA%WC9nZ>BKH&;9vMSW_59z5LtzS4Q@o5vcrWjg+28#&$*8SMYP z!l5=|p@x6YnmNq>23sQ(^du5K)TB&K8t{P`@T4J5cEFL@qwtsCmn~p>>*b=37y!kB zn6x{#KjM{S9O_otGQub*K)iIjtE2NfiV~zD2x{4r)IUD(Y8%r`n;#)ujIrl8Sa+L{ z>ixGoZJ1K@;wTUbRRFgnltN_U*^EOJS zRo4Y+S`cP}e-zNtdl^S5#%oN#HLjmq$W^(Y6=5tM#RBK-M14RO7X(8Gliy3+&9fO; zXn{60%0sWh1_g1Z2r0MuGwSGUE;l4TI*M!$5dm&v9pO7@KlW@j_QboeDd1k9!7S)jIwBza-V#1)(7ht|sjY}a19sO!T z2VEW7nB0!zP=Sx17-6S$r=A)MZikCjlQHE)%_Ka|OY4+jgGOw=I3CM`3ui^=o0p7u z?xujpg#dRVZCg|{%!^DvoR*~;QBH8ia6%4pOh<#t+e_u!8gjuk_Aic=|*H24Yq~Wup1dTRQs0nlZOy+30f16;f7EYh*^*i9hTZ`h`015%{i|4 z?$7qC3&kt#(jI#<76Biz=bl=k=&qyaH>foM#zA7}N`Ji~)-f-t&tR4^do)-5t?Hz_Q+X~S2bZx{t+MEjwy3kGfbv(ij^@;=?H_^FIIu*HP_7mpV)NS{MY-Rr7&rvWo@Wd~{Lt!8|66rq`GdGu% z@<(<7bYcZKCt%_RmTpAjx=TNvdh+ZiLkMN+hT;=tC?%vQQGc7WrCPIYZwYTW`;x|N zrlEz1yf95FiloUU^(onr3A3>+96;;6aL?($@!JwiQ2hO|^i)b4pCJ7-y&a~B#J`#FO!3uBp{5GG*Cni@K85&o0q~6#LtppE&cVY z3Bv{xQ-;i}LN-60B2*1suMd=Fi%Y|7@52axZ|b=Wiwk^5eg{9X4}(q%4D5N5_Gm)` zg~VyFCwfkIKW(@@ZGAlTra6CO$RA_b*yz#){B82N7AYpQ9)sLQfhOAOMUV7$0|d$=_y&jl>va$3u-H z_+H*|UXBPLe%N2Ukwu1*)kt!$Y>(IH3`YbEt; znb1uB*{UgwG{pQnh>h@vyCE!6B~!k}NxEai#iY{$!_w54s5!6jG9%pr=S~3Km^EEA z)sCnnau+ZY)(}IK#(3jGGADw8V7#v~<&y5cF=5_Ypkrs3&7{}%(4KM7) zuSHVqo~g#1kzNwXc39%hL8atpa1Wd#V^uL=W^&E)fvGivt)B!M)?)Y#Ze&zU6O_I?1wj)*M;b*dE zqlcwgX#eVuZj2GKgBu@QB(#LHMd`qk<08i$hG1@g1;zD*#(9PHjVWl*5!;ER{Q#A9 zyQ%fu<$U?dOW=&_#~{nrq{RRyD8upRi}c-m!n)DZw9P>WGs>o1vefI}ujt_`O@l#Z z%xnOt4&e}LlM1-0*dd?|EvrAO-$fX8i{aTP^2wsmSDd!Xc9DxJB=x1}6|yM~QQPbl z0xrJcQNtWHgt*MdGmtj%x6SWYd?uGnrx4{m{6A9bYx`m z$*UAs@9?3s;@Jl19%$!3TxPlCkawEk12FADYJClt0N@O@Pxxhj+Kk(1jK~laR0*KGAc7%C4nI^v2NShTc4#?!p{0@p0T#HSIRndH;#Ts0YECtlSR}~{Uck+keoJq6iH)(Zc~C!fBe2~4(Wd> zR<4I1zMeW$<0xww(@09!l?;oDiq zk8qjS9Lxv$<5m#j(?4VLDgLz;8b$B%XO|9i7^1M;V{aGC#JT)c+L=BgCfO5k>CTlI zOlf~DzcopV29Dajzt*OcYvaUH{UJPaD$;spv%>{y8goE+bDD$~HQbON>W*~JD`;`- zZEcCPSdlCvANe z=?|+e{6AW$f(H;BND>uy1MvQ`pri>SafK5bK!YAE>0URAW9RS8#LWUHBOc&BNQ9T+ zJpg~Eky!u!9WBk)!$Z?!^3M~o_VPERYnk1NmzVYaGH;1h+;st==-;jzF~2LTn+x*k zvywHZg7~=aiJe=OhS@U>1fYGvT1+jsAaiaM;) zay2xsMKhO+FIeK?|K{G4SJOEt*eX?!>K8jpsZWW8c!X|JR#v(1+Ey5NM^TB1n|_40 z@Db2gH}PNT+3YEyqXP8U@)`E|Xat<{K5K;eK7O0yV72m|b!o43!e-!P>iW>7-9HN7 zmmc7)JX0^lPzF#>$#D~nU^3f!~Q zQWly&oZEb1847&czU;dg?=dS>z3lJkADL1innNtE(f?~OxM`%A_PBp?Lj;zDDomdg zn+lVJBnzA5DamDVIk!-AoSMv~QchAOt&5fk#G=s!$FD}9rL0yDjwDkw<9>|UUuyVm z&o7y|6Ut5WI0!G$M?NiMUy%;s3ugPKJU_+B!Z$eMFm}A**6Z8jHg)_qVmzG-uG7bj zfb6twRQ2wVgd)WY00}ux=jqy@YH4ldI*;T^2iAk+@0u`r_Fu(hmc3}!u-Pb>BDIf{ zCNDDv_Ko`U@})TZvuE=#74~E4SUh)<>8kxZ=7`E?#|c zdDKEoHxbEq;VVpkk^b&~>-y`uO~mX=X0bmP!=F1G1YiluyeEg!D*8Fq-h=NyE-2S;^F6j=QMtUzN4oPedvc*q(BCpbg~*As!D@U z3(sz|;Pe1hn08P_cDQ(klZ6 z;P`q(5_V?*kJYBBrA1^yDgJD|)X1FV_*~sO>?8Sy~I9WdK5K8bc7aeNC zDb{Fe>y3N^{mrD1+GyH{F?@9}YQ2Om3t`nt zQ(}MS8M?6Vk>B=*j*yibz6QCdR=ALgTUcKx61){O@1WkPp-v$$4}e#KgK`HG~2@#A?`BF8em`ah6+8hH-DNA2>@02WWk9(fzhL_iz|~H~qEViQ(*{ zV;3tjb<%&r!whm6B`XtWmmrMWi=#ZO&`{h9`->HVxQ)^_oOS{W z!BzVRjdx5@pCXl#87ovlp<^QU;s<*d$)+|vI;Ai(!8Tjll^mi6!o~CpnlgZAK>6=V zm38^kT`D$_$v@UYeFyVhnsMZI1m`E&8<{V07>bBEI1=fg3cji*N?7pBzuamD`X|^^ zm!)2v?s|6T&H-_^y`KM&$!0!9tai9x&)5<(&sY6B`3D{$$KMAX3@&`SW;X0 zB-}obt^I;|#o_bR>eOv?P>=UC6CGTXIM+lSu?Uy+R9~O;q|c2+FafBP;E)B5M9HJgRIpF|GvRi*E+JTBI~T?T*X}r) zefUd*(+3n_YHZZS(g8)+7=pNV9QR^>Qs8t+iEpbJS!9;wio&9rn=19C0G#Ax zM-tWHp_YlJvXWsUqJUr^`OYFA4wkgL`cSOV;w4?tp>GT1jq}-qPoN zp&G}*;+#+Zh&vqDOp>gRL#^O7;s2yWqs+U4_+R4`{l9rEt-ud(kZ*JZm#0M{4K(OH zb<7kgkgbakPE=G&!#cNkvSgpU{KLkc6)dNU$}BQelv+t+gemD5;)F-0(%cjYUFcm{ zxaUt??ycI({X5Gkk@KIR$WCqy4!wkeO_j)?O7=lFL@zJDfz zrJJRDePaPzCAB)hPOL%05T5D*hq|L5-GG&s5sB97pCT23toUrTxRB{!lejfX_xg(y z;VQ+X91I;EUOB;=mTkswkW0~F$ zS%M}ATlKkIg??F?I|%gdYBhU(h$LqkhE!Xx$7kPS{2U4wLujF_4O+d8^ej{ zgSo(;vA)|(KT8R_n_aQ$YqDQaI9Stqi7u=+l~~*u^3-WsfA$=w=VX6H%gf!6X|O#X z*U6Wg#naq%yrf&|`*$O!?cS94GD zk}Gx%{UU!kx|HFb+{f(RA2h+t#A!32`fxL}QlXUM{QF3m&{=7+hz@aXMq*FirZk?W zoQ~ZCOx>S?o>3`+tC&N0x4R`%m)%O$b@BkW;6zE+aBzeYi47~78w$d~uypaV*p$kQ zJf34Q+pp~vg6)yeTT&qWbnR2|SifwK2gA7fzy#W(DyM^bdCjnee42Ws>5mM9W6_`j zC(|n5Fa&=MT$$@?p~)!IlLezYa}=Uw21^Fz-I#?_AOk(7Ttxm;#>RDD_9EloqhvrS z&7fpbd$q_e21Al+bcz|o{(^p}AG>jX0B}ZZRfzk$WLbNLC{y|lZ|&a(=bOE6Mxum{ zM=Nd+-I2A-N&2giWM2oAH`O&QecJn6%uYl0GWlpx&2*)BIfl3h&2E(>#ODt4oG}Dq z__73?sw2-TOWq@d&gmYKdh`a}-_6YQ5```}bEBEmWLj))O z?*eUM4tw0Cwrr+4Ml^9JkKW9e4|_^oal0*sS-u_Xovjo8RJ18x_m7v!j$eR@-{2(Y z?&K4ZR8^T{MGHL#C(+ZAs6&k}r07Xqo1WzaMLo9V;I<9a6jx2wH2qeU?kv25MJxoj zJKzX`Un|;_e&KY%R2jU~<5lm-`$EjIJLDP~11_5?&W#t3I{~+0Ze++pOh2B4c1Mde zSgj$ODQQm7gk&w{wwfE1_@V(g!C=2Hd%Gwj{{-_K4S|nZu+vk}@k(?&13iccsLkQo z_t8#Ah$HVB-MRyzpab*OHOp zl`$tEcUcF9_=3*qh8KTaW$znGztA7Obzb`QW5IQN+8XC=l%+$FVgZ|*XCU?G4w)}! zmEY+2!(!%R5;h`>W(ACqB|7`GTSp4{d)eEC8O)Mhsr$dQG}WVBk$aN1->sTSV7E)K zBqr;^#^bZJJX4E_{9gdPo8e?Ry>ZrE&qM)zF5z20DP0`)IIm_!vm&s2mzl z2;EPI{HgFH-Mp&fIL^6f74>19^>o^AOj`uyL0+Nb##Slvi9K4LQSs>f+$j?cn9Z__C zAkyZ9C;#uRi3cDYoTA>AT<|*pt{K70oZKG*S1F$r?KE=$4~W3!u53yUvh~(kMrClS zXC?Dmgv4iS`>~wBPJJFL_C8x2tEg*PCDX2=rHQ@z+Zs)Kkr;FYG`GnbUXqdipzvHE z1aZ>G6|e`}Q#)Kru0)(SZnUCN#dN2H zd1}r&xGsaAeEed9#?|0HzMGA7pl2=aehy_zsRV8RKV6+^I8woDd%4J8v9hs$x{ zl*V61wSumovRVWtetd1eJ%i^#z`_~~^B;aeuD`6LgHL66F0b^G5@om^&_3REtGmhz z%j^9{U`BH7-~P_>c_yu9sE+kk)|2`C)-ygYhR?g~gH`OK@JFAGg0O)ng-JzSZMjw< z2f&vA7@qAhrVyoz64A!JaTVa>jb5=I0cbRuTv;gMF@4bX3DVV#!VWZEo>PWHeMQtU!!7ptMzb{H ze`E4ZG!rr4A8>j2AK(A0Vh6mNY0|*1BbLhs4?>jmi6fRaQwed-Z?0d=eT@Hg zLS(%af5#q%h@txY2KaYmJBu>}ZESUv-G02~cJ-(ADz6u8rLVECbAR7+KV~a!DI83H zd!Z(Ekz%vjA-|%4-YpgfymMzxm_RjZg%ruo zT4^x)f*%Ufvg_n`&55cK;~QChP6~Fy_Z67HA`UtdW)@$Xk-2+|opk6A@y0~3Qb;V% z%+B@ArKl|Q^DJW&xuBZD#~SurH7XXf*uE0@|ccNd&MA%Ts*1 zg7TU!xY}~*AOY+tAnFR(Fu)e@^9V!Rm65$;G$-?6e%7w7p9WT098%-R?u#J+zLot@ z4H7R>G8;q~_^uxC_Z=-548YRA`r`CsPDL!^$v0Yy<^M=Jryxz5ZVR_<+qP}nwrxzi z-)Y;nZQHhO+db{>IrD$#DkHP%swyKhV(qn`H9~3h0Bd33H*DAP0S!ypZqPF^1^tZJ z{z;HN?$WJ5{0jQNzYOc|KbJ(Pr42~YhW5ohNdY*rEk=({8q+F}hy)&ziN(@q1;>jL zBN<9(k1N!p2D%uHF0NxFut`XwEMc@ZH-|95>U)PY@}C=bmV_*dakL}J5DUpNZi-y& z+{i0>H@c-g|DBO)HJ>7$VVtn)z3X}H`FuN-t>gcqLas?Lk@MJb5?u@BTn0Q}E(}S~ zXrNX`ysRv*iOn1v@fBDeSDvvR>+;o>kj ztRqEZOWN!fqp(`XQ3ppvC)c{AeyS6b_8pN1M*~0=$U;P31!~Px`Obrz;GNs(8RrJvONy<{Dk1x0z zJJzhQBt{J@&DP6cHugB!q?xi~O`yJYHUsTI zmgulx%I<*?vPSl(!tj;LL$K*k zH(*d31iyB9aYAzw49W&qDi0>f;b5kA31nz(%2W`QFJqaX0&hM`KP1gfdRw?7@}$XB z!^cUI%C!?X!QVQxbqEFSbuP0>_3MTCof6!e4LMAfGRd0;Lt+w0WK@b4EkGHRqX!h{ zrYxwwH&-fM67X7zP&Qpup&vAOaKH|S*pcbI{ksFg@tfw)paaK)5khkys0GSTnAtfC z{mVJkCXt|G-SYwt0O4dM8Hf{L*&^nOeQ271ECyc5Y&z5R0%hCq6~} z$XW$kcz!nnCTAl}NyB0#ikwyg_M};inG%*x38`EYJ%FXdj&A`g)-wJ(R=C`O^r{W` z8$1r{G0X4g`uD+}vw4`H5!*B8TTsmeaYGk3x0{&aar7ocO6?dlGbyV480<#{%^93y zF(ei<%{OYi?n?L9#HL_R-00#zRzbbwVnJ0zt}4f|KNBkT6&=Kb=$E(@aC03vU~p)7$XA@ zq5*`*4Y&u*=Ju>+x}q&Xxsjn;Dd)6Otudner9zi z<*LpeG}*vJ58#P4|qXF-ul1|u*;=-@oGPtmBnQW6VY9(s`5GMsO@!;s_PKo_? z3HbGokZ|vaAA-guf5W0JDwpV}1u8;7XJ=wD;NgcLIJW8S5w!c%O*zU0%~)0M)`!Al-+OFsmPW1zniB%fqF;klqxz`Y z2@srWa3e?B3ot|nhE|Q7VIjr+$D7F^n?wm5g8w?Ro0i72K3u^g)&&F^9~@eHd33YY z9LR!!orc0vq$sd~eR~hW{4?R3Di;~mz{^G1X?#-!|Cli(#0-sm|GHYpcab`ZA=zi3 z5*m>sJyOij{!PgIJa?A0%wL*Ur1fLJdJW$a>&Xj5p_IO=SwyTp@nn&@6L4vIfT79aPyo{LQ4DhIz1 z5g*+hII!(cLGHc5ROH&^^o=02r*x>MxMPx{JFMmNvzJ?AI8p!u_H8L1a`{6~bF@L* zxszth=`>%Vi`=E{jJKd-+6pf^vo93EzqFfTcr)A&V{rERu__UAQVyE1imol78AFmB z7T;pNFxW^M+O3#;Tz^e*`AqsD?M*wPT6pnBFPA^kOTnZYHr@O(JUQ^#6bD&CC*?HG zRAKSXYv9DU)L{V(wM=te@V@Db3}97Sn9r2nroOz06!qV=)+%EKB^MR_K}p$zM5OD1 zzhYv+?%A`7dBrU(#&1hXF;7lzH`nENZKP2I{qp^NxBA8~N>?1H@uZ~Do{d+|KYx9I z_z)J7O(;xu0%0n3o4y7LnJKRPK?RV@_v_YLogYPH;}`>cZmDVyO#%-IMQVq6z9r>@ z?*AQC$=?|aqrY8xGx%vfk0ZeByTz18IrP0XTVlJyRx5!NALYPyjcn|)U5jl^<)_KZ z2C?1|dkBZ;h8e#)3gUPfdf80xu^8evspE%Xf~x zs%phX&YuB{y}>%PuOG>s&EW}5Y0`dyseV)!C|`1(U{Nd4c4>07ZFmdTJS2T3+dEw8 zK%f_x!O?H8+_Qd>$DsYNY!?tC^H;N+!fQS{!4-9c^;uXx)D3|joo_FlBTTdDM4nx{ zPve})D_u{PG>&^G=>$2N-dZ!eMx?9X7FmPNo)7|>Z|A-mNZ0{+884L6=f-{Q4bN3y zAWL{oJIh(js2$bDTaV&bh4Fn=4^M?@N~+$IXxytdnI4{RkYA$8j(}sb2TO$~49JHz z0$K$WB@axSqKsyG>m7&3IVR+?xXLfs7ytuJHH8{`ewhkH;?H7#an)*hPiBLi22jAI z{|tZ;dU=nDUVyfIurEm0VoB6kiaK#ju6RV?{3qaV`NQ4&$)fc4AAVKiXu_1$86nxh zX)Mif*|y>N;S~7UCXQhs3-%nqNuTu>=8wqtp$-#tC?bwc-{&k&0>0nRBku-b5X931zqll&%fn$1$->@El+EIA;L zfEYJY)kaTI%H z{A%hpZ?Xt=;#(++B0e)B>4_a3E7h#8upWz!G;VQBX0rjzKvy9N2LECS2@wrBoS;4G z1PgI50DD!wtwsZ&JoAGuum9s&+0NI&_n}!kUTvpD{tyG9jlSXyQ)m9H8VXoDY$j!w zo;imjJKl;E5u|n4Q?HQsy`*&=VY`SG+YFUqG*+;A9(wKfm_|6^SWh_6>1u63)H3zEGm5Uk)#z>J0XC1L+&pzieqnAo+7zlr$M4kl;-h zjo^h7U5Y3tbY@(_{#h1et^{nbOP9Nw*tJOD;WejSG-4d{(2X$tDM@-rK8SbUqMe}%IPqxOV}m#%mq0)auvNwT2R9)$1-o(2o zpIS;qwy8m^tEBC99O}bYKd7ALbB~$d<=eGd>WML+U0aAl>{Uc8CB|oVWMt zbPe9+6&V{l2Th1)Jx`K64?gUC_<>x#Wk*SOSA<&A=j2q zo_M`Lznpsg1h-W546hm(q@Rf=xL@w5QJ;HxIp?O`;sOMovgc4n%D5`kiDO6%Rhe2^ zzPa=8pd(2&HN-=5JzsiJ^(ZlLVpZD^5!$(rt0PVLQCzh7s#6_N1dRKtQv_vTgSQT5 z63+e@K`67zjbb@QdwMNF8G29tcxAl36SZAGxolCj9aS%>(Tl*6a0eW@3j4!&d!12v z%+~Xc=>VJqBcW!D#JX3#yk4O^;#|O3!ol;J%t8>wc!*6`+`~%?-QE_M{wa&vg14R~ z(M1VT-&l-M(N1>3pNjVfvCIk}d|H4&*7{*8!W-;^tFgD31O%~NtUaK_*-m7CSEt}T zm^Z02X#cQ$Mcw}TG{>1I`vmvNoxujnPra4aSwP55x37=0VvyV<)68QB-b$o-h7p*V z#QQ8?A7`=m`*+dTfYdm=;i1ptR|In}rUF^r&{bKbI@5DT$JEo;?-N}Z13}n16v?G2 z{?@ny^7|!rg(on8b97#GupiPA<(g=o;@P`4 zEx06)SiGKkIKFHzK1M`ctf?vQV#b-{ws=+0U^*LYoTK*pu;A#NB$$I=Tv{LLVQin~ z@aGTp?J<(c_1M!Jr8MK;XA8fcB+*DkFF@oAhQ=B1o*$<@;ZdGs_5O!BKi8XjF2L4n zA&(?SaRDWm+p0UTFXj1prs!*v$(q+s=8S1h(*H8pd5*8%HGN0mgw3yvfsxr4QYT)o zzdjal^6zA56|Z@csYH^3Qr2~ZR#p|Huuh0Yt|$~>oQZJDF75aeH%UlQv)fQ=3P{i1 zRt99gL`$b61Q`pdos?W6yd&%2IWK#}$wWOa9wJW&($J4h0M|9sFtQu9k)ZtYEQ#vu zS+uD(3`7T~t?I;f%z8N~nG&FVwxGXrTL!k9s#LB}FSo;a+V-j}H^myGwQq@jTIycD zP5A{w+a;^kOQW^C%9W{j^&o@)3!v~U(?wx42E5G*bd82&a1p6ax|pk)#8nG9risCw zOERH8;tq?Q4ymxf*9_aF-sTpLvETwD#sB#ID1D+WohEt0s557Ij5)ldexY+diQJ*l ziBo;1v*vx(F|lI8udAo450QIQTmPqf(7oULr5*0dE9i>i#D&k%WyfM*4{*?_%9k>g zg1_1%x?#`Xm7M@YZ?!zJs$AxS&8sBLI@c|-vSiG<*OZyw>CL*p6#N~p z#VywqpWdZ;{ylc5d7W8E7Jx_H+5e#N$h#{ni@#TlGqz`yah-qCC_;P8?N*>CPJ03b ze(YVDvbIR$#lJEkuf}L7F8q$fKCWz&>{uFg9JgTOmA*Rux-{|#+pO`!s!!4;PlE%9ys+;|)oK%&V$*FH!G2%|y(zz>X zUwdXer0HIIJkelANg_W!ofsyiN{zi2=}G1UL{`V81}1D1Sz zviLV^w-$RE9fE4@H+ys>u;OY!sgqe&V-oFE9Fn$P9HbpOI{}esLIvc zV5S-9(XjFzn1qzo2owwg_d%7_)cR*!d&%@S&D($cFFMXXd!GdUxw5tZ_W@zRbjVfU zzx13(Hc!$teqA2WOYo^+SHpRz16DOcYqaXHSMZl2Ax$)f^WC??al8lfX9)O_p9#Ml}LB(N8yJ! zj&_UD9K54Rt#yqvhklEMZ3bRC&)(^h`#kzq-#_QN?J6eLT$ zMWG-mP;HkB@5;2*lAP&1*4C)HWEs{gtp15Y%y|*%(3UOMu*v4kTi0@pWvg2Y%7yI* z%XNlZa$@AZ(Z#Elv`5MUei~VFCjF8El)@g&>(v;E; z;laavf&ANfk9*0LA@oP4QmbCBF-lB^Mj~wo)eGG57gqAKC>Hd80Eb+7b;iJzV5RsL z8>ddQH8PnC;l{M(t4c$M=q78GW6=*d#c`-jK$q#-{9c)UNO4eLm9c!DWcCth4O-FU zboSKPhL-lq3q<)m8Xw7+l=Z)H=rGgMI0H?KrPjc;iDzY5g|Ve$8?SE`8*sb1u*>dm zD~f9~j2H~6Oo2`_1 zq@_mmUbFQV25E7XJ)zBRQktT12@qHHy-@aCdAFWv4iZVN0B3}E;k(jg>X|eqOrqgM z4yBUuA*BHdnN9v;5>3#L$NFREyHW&Q*rWYa_q zhC~>M&bMFgXC6AeQ`P-s<}Ot_x^cb51r7ArPbRRs&Dd_TEeugnjR(O#V5i6OYjzRF zw1@Rvo;_wEfQA@P%I^9ljrhxxuqf9g^cWSKq~+kiVxa`&EBDqmB=C1G+XB7`TQeiV zR_k?`$&W&+ntIPeEtM9hqcj|yfW>x7&1Ht1@;!d#Wo%1hO+^Q{E?VD|`-OvV9G?tp;6{sI%L-u)Hw z;|`uN6~VqZ!g~K#B@W7?wDcbO?XS4hnW9kS1Hbi=U_m*~7`N~3oK;qFTX$$LQ#CkL z6I?a(HkF8SKJU8mT{K35ekfP3`05!M{gmrV0E-=IyqP=N;K<&jOnPcjdXrbk$%)z9cUe|#I0unK5^+qGx8#2 zz_!bmzVG*Uat*&f4P>&sV2RswlITV}wPz?_;(S;19}e}54fP|K5l_c2kU5(-Zh!7t zz=B2HktD~ap{s%*CDEl?x6o+91T-xH895-S1}M=*KhFM7Nm&1$OB++Robv0T`OBcJ zXNX%Xio0_ryjr)!Osc7au35UM`B}Ru4zN_o+C!+s&e7|}Zc;5?whP$@J@DE`>w-XH zlVmbrI4|-Z^2^I^EzuYKD+JA@8lx%>aLFZq7KT1~lAu}8cj$<-JJ4ljkcSA;{PNr)d-6P5Z!6Q=t!t*8%X)a|;_92=XXN=WMV))*gWR-wHzU(G6FPTfSjd9) zm8e1mfj4qFmlXO*a3};$&jgc$nfG>NR&iao(jYk`%E75h=K~dJ{Jqs%UH|aGHL8)-1MOyS2B?OJsyeA_YbGMDpE+>=NFcyoI;N z>1>3G4QR2~EP{L{x2e@E1U0jGGV5H$aeigDq&Dr zQ3FwJ+& zndX7VK+XD)t06uUY=)Cfo!ke%uDpOmq^bpEB`iv6(CKTGgEZUi4ddfNXJi_z4;)ob z?R+qj2SYX*zi8z=DXChEEDW+Cy>w-0agE|A7MoRJ4}-(|go-rP#sr%a(5k%wV z&Jllj+6XuSoIfZX9|mK!bbd)7TuaHBvoa(`9C$*XUh}hH1;Q7cTJQR)c>h}Hfr$aS z64c7#D^f{mN3s#2=SEf1$(*Vj{vZjF6Qc{a=VbTske7L^EY&A1I1sgXaYSH7(lF1V zZ<7`Rq33WZuu`!HK$wRr1=uE}#&JMftnZ&(P17gWF;>$TA&$ZQnIz>blTrW@49Z&H9yhgLBpFw(57K1dbIQW4fn1X(IiFWEKmPzV8gAa|ak)HAsmcQ7stP|q0hEzBNL=4YdXEkyfS zF+K+CVB#~(qd7eeZqR-VKIYJVmK2ePk``4I^PfQ*C7NUR z`w9lb?iHv2$4_p-+a+O}Fq6SnPiz>aV!~d=l3VdgDuwAPMR9eR`)b_`lg~{oX0lf1(zbBrnj4+-q zOl^#`)XKn=`()B-jExviKVTYrAKa27KAg3cboG+}D6*R;<`GC-b?i=e;aV7n(}XDS zK5xAEV=T^r#eThV+3C<^H>SuvAP&fw;Yn67eY%4=Y(p$~!`~h12 zQHM|f0#pQP_s$Q+TtMMvBdjQbLWw9cW?gl_+P z)2T94UJaYG2!yXITYjYl-@#5_47g{N|5=P~m|e}-F)*^L+{7O$#wv2e##5Y=A{>jN z6NhQSor9ulwP3gfxTF?V`P7AJ#E)ij$I`gc2fnmp&9w6qS2-Ct}6 z$#O%mKtP>I2VUBMt^Xm3LjP*D=xEyV?|8Psb91ZEj=gM(C3^Kcfvbx*$NK+MhP>W;OneZ{Q>eFEmxv}%ZCJ32=zr_OZd>6~v@ z6+3JzX%9qOvKS393r&R9O+te&#?{Q9nLkOV-eLg9!{WK}WyUWLZ7bQ5u26*u9c*T1 z_s1)j1k5&b8&5@YnmtS{tsmQaLW2%8D*8G-9w#PcVQh6sQY`!tBpU=8EZR!zfB{f{ za<+Err#ZNM4JEx5n9!zuC#KmeI*%tRXP}jpswzymT7J{YpXdzA{J7K)j1tBF8B3DL zZXkec{`rT_{__t_`!E7veO1rg1tFzVeUTBjut*3ZOq}A$r%sWXn4v4|rA+7uMvy9n zL~2WHKLg$BeD2Wq%?frTUM^c}?K?3#L+Q2-?PR+e1Fn-XUThl8^}8JOyDZz-wcFh5 zYJCJ%J_Pf~bX(0A?Z4hGw(mY?J$j#Vo&@9O>in*f)*`H6&(Z-5xx5}$V@dR)-lxgN z=DMA_EJO4+^w_+D7N>4=%{6AbvpDG<(b)xE5Ezo~oEg~cEM?mwyY?3ZtFE;RyDS`u z(^sa_s%B<)vktqh=1|?Uv6DXsA`D^B9%_mXqx1C=a#KurOE?49)P_ixiHAA)D)oqEjQ6_v0UC9mTtMu&kf8&7uRiiigPD{$Cf(&DuOj0 zr*5{zPyO@Kq(|Ttu@wxKanV=^OPOjh-_$MbNz})ou6*9nq_XQo86WJ@JN~-b=Ln_8>Nz_ZS#QpRGt+bzH*-;{#x7PFqie+ z7p5e})fcDq)J2z=z~%nrFGFjbVu~0ICDHW3=HgtCW)?Z(%Cx$z!QuszcOCe&3!Al2 z`793RnB{Jj4QpQ2N#oKT>aY~aNxz_6B2&vPdJadbC4qp#H^<@o50}m>7WR?NO0$ZI z9OKTM+jxMFWX9mi7(@j)1Ji6~?HLU!KT0Y5a^-?|XH^B?R@T zn&a_U_XFAsGrNX@S~g1<=uz@~dCcZO=1??VC@PML{g}lbuN?j|_1S=dJgbT~o}}hs zP_uYZ&0+mWY1fupe(+6nn6<9-)Xluk97yX-!!lqSXq~!kL-=+4$Dy>O$sKO7M^1QY zhZGZfiNQu+?sef?E>5sqj$kHmf;kMv<>Gu)!^4!#7T009vBzq(m2aoHu#+93HBq7T z;Fs8IHvUlmxCB2hkDbm&xwFQcXUD_&sdeu|EYhFpf7v5_LCcVua9aunVe)qoGmyg# zIGlj&IrLKg=id@t7s916d&Gf(%X7^FFR9^bz-;*o1~Sa=`cKfJ0i}X+pBKN=?}!dP zg`ZMtP6xSuvHb=5HYH%ELaGxwqH{ zpY>Ic^}J!OwM!VmNM!$nUg$qN9DLtKuBvn1(x-P+tA*UHoOc727>5?^J;JFo_ac@) zU57%w^U2ME z@z^ZsB!AhyOscE8;~Ft$)NL)GcLteq4d32fw??L0QuWt_M9IJMgZ71Jm%2khx|QN+ zkm4zQ@OjyM+l=Rv(!k?%cYwnf7HWs^M+P^zo5o?7;E)V0v*zf}(;?ms0oUK)wKmZY)mSTGN4X@2=ZU!Gy73M(ftmHJHLFKQDcu`d% zeqiW{G`?}AtEP zKCnHuWzXZ_Hc>{cP@h~M$#q}kG{52%zmhATR3AbNGR~*6(%^Gs@UZ3i%7%PJ1mB^S zcdcrFDbD6lEJGZ4k6JT;eB_JbgIkkOqkz0I{q`d^kWl6a!%w4V?Y!;8%uU(-UA4Ti z{pv2+5CN^ba{ALpu1&qm`sMP@_L=-a)@-zC1*`f)uV5MU$xJj51%?S^ zoo@;kqY@4Zw0B!+hIvTT8KK*~9H@u54r>s{MX_|#z`Z$55bDJo#=hz~k)7CTbf>Gn z=!u;@JViT~(>P7UDdIOL;6kPDzOZNl16jLo5tHS4a%~T&AlicnCwZ5pZ;+WIB3tJE zv|J^!X0Kb|8njISx#zoB(Pv#!6=D}Uq(6Dg*ll##3kfDxdHdBXN*8dZOM0I{eLTO4 z=L}zF35GJX4Wee`#h=aCB+ZV0xcaZiLCH3bOFYTmEn0qf?uC#lOPC7>+nVeO1KQ@S zcZ5Z0gfk8hH03QrC@NnEKNi15bWP;FEKsGi0iUHN4L&2_auv%tIM}UFfgRyp5HWt()pn#0P9+xF2H!8zMqf`WJ*9YB zq~m+%xLtVjza4>CO4*%thB2k;Gv1Ani%8)IP6Pm^BAigXgOUHWcQDEgB??AtdsOx5 z+pXKfU4>+8ViRUJ;h()e88jRLEzSN7%O|=MovCW3@VxK@Z*xS$WLG=u_Nenb0wP@Y z6zs##uQ7oFvcSdh5?6kZ!%8l$Xuz^Rc!lv4q?e$mv(=#@x)s_VFF50vGuE_Nr{4zXB>y?7FOMC5^sBZr`mS*t_@%LYN9wl z+lsqD#V5JR63GEr9^&9*f)kFs zJ-A(>>!h~d0%9*wd+AY+&oryzurfV{QP{&-AtDs}#iq;dal?A9jE;huq2gExb3z+- zVQB@UHlVfsy1$)dF`dcZuc(GLnim09jrI9nJ6<#=03FVrkuINg2`RTPloS^^@KYD6 z1-C-Oj2OI0y9Tdx>=dNHhOYVvx!J#4EMhold-PGClLuLA~k2VDl6cPuV4lI5c(w9@7sllth~H@)0+v~XYqqC6&*fSX~S4Bii^0& z=M)D(5FoZsKxB&M$J_7lbS>$kF=@B|Z$#D|LHJQIr$aO51ta6s96Ug*Jk;|>9Yd$! zoF2W+)lFzY)J<>U$PHwbe9>BKLAeo~e%=Qy#qhvK&`)b2 z(U9#8bba`eGr9tr$SvM4`y`lLavOzPm`l<%-(R<1urb(AX0RE=R=#&QI)klkwrJ5%D5YHZ!~s zGwK?zKZeX|uO*Y|xLjO#6uzO%iXWsSE8#zLOWc! z&2L8sdT;bhUW495)_fGCcOLM-@DfGcb1xjf(ezYJxYOv<7YE$lBCrkbfBA{`I(GH- z(yHy1h=bg~fE$aIbB_3l`|p$R_p0b(+aL(~b<-Am9H@?s!T2*7{+*Vj?pCpV5&WJO z*GbW%PLj|(hbd!fQK5Y-kgDHV!-I$y6G>Y|&uo9+79v}}$s=l$>#F-_F{TjUn~-!M zBN>n)@(LkzI0Sg?f1s}uBZi`wRB}ywU7wqq-PwaS%3nitaXb{&Q=x!xvOPfiQmmkd zWpe2@y7?wbI;hF|hlqf@x+3@a4$wLdJ1PZBoRc9oRGgdM+vm*;5XBZcMZ+@4_{aPUS|`NsD4YP2JUM zZEvA&!QLB$K*%gHy~y-RVs-C zkN^usP)S1pZXjj)nugy#?&vpiE^DS|QlhiBOc?nC$9CK}Ze)ihI{p-m$pgYV^5L~B zQTU>)x*fvKCNK*9j$@Gyt@@I2LF8c7YvDJDCf%1h0zVyNg7E~R$`6JE1EQk~-c1xG zE@xT)TesWHs}ny!5_7F_AyGL9K?Q~mP?>Vs!(oWZR42kf?*iTV*h5>tnzpljZL8IR zb7}l8q%Ckfh{^e3k^3pQMk=gLu60`Ja8HdkzVbeAU*exs*ajmRVp}O}l)TqX!?G7e z{4-~g?Gq%~)IJJ7p1k*WSnL3jqECe1OU}5nirS66_-$3FzMT5t3X zg{jgP^5?%zb(vMa!S|1cOYk4W!vG2KKd{YFIbPCk3_74HL`fWJASs{fxpzY@$(}Q- zK5I4TKS~`mfiDoDOm;XycF6mi|K|+d=lh=@U?9_V)BDDaZAnEw43`Ls1677I-+uFi zG?^$Fbc*pPun65{D!fH=3Oyp$WZAY!{JhzaUtIgYCWXf@)AkTa@x4xGjp0c zs7@JB012~&;z=SMbCp8d=Ga{l0(iwx<@o(f!OwmyH-gBN6wewq7A_h)oKg)koFPft zNfdie%F63S?rGDQR(N=bPuK>G0t^ax$0P8`N_cvR8rOf(O9T7$9#5!B;#!XUpLZXu z5C(OESAmE*2+hV}!bg$4K%`cQHBk!>##tW>1RbC%am`*|5IbvoLh!BqpAi2OmdXqf zHp%|!N;d!LN_26809n^14YVJJBe7aL87U~>HZ)VK%d|rZp(~zwNH#VGuX!vfal&Vv z-c)h33DOB@xl*~m5ZZ22sVRK>8I9+)QMVtsAB>r~SMkGMZaQ;Xi|?~Xxnmx;cYwYx z^nNxRxGcq7I!sO#b%$!0vQ(OqXm6T4mTilvMlYj|*i|=MK%kT2df;bZGW@NrgeX>( zf7eBsjJv}pNuEuHPEs42>}a`ut-O9lZDNh)_CsBpeHKvPKnpcWh^bC2QtnB5a4qy) zSrZhafuAkk5{yiM|zdiecKh zuc2R;6^;@i07fmepeofAJdX*knDzBA{3tyVYu6z#z;Lsi&x_bzzLEpfXtH*NrY_G`= z^X!;eI#hV*mmjjEOlo{TxQwSdUv0P$!Qvijpv9plBI@FUU#RJ)8Vn1ZGA$ATqF&s= zvcTS>Z8pepd>k=sjPY^3fpCB@aW8$Oq%fW;R?GpYoT@ki@N#2LxgTk1dYZHNrk@lx z7=yYr0FT$I>z~I0nXpPp$t3)}D?2^<@KWH#E{irFy2`)5r{AyvWHYzn`5@h;GVj0@ zJ@1fbD9gX=vQNR7PG5i}jFE}9#!;ote)FHdW?VVe6v4dWEz(R?!HC4KeVde*DGr=F zRotamm=!I~=_{|m;mCI4#5{C3_gBXan1<>!K!8O|)&K?O_L`}=uKCJ-s&+!XTk?wi z%Bwa_&k>4}`a` zFCG!c^Cdj#Bc2z2PXBCW$G)<%9X6;oZiigwvMLXQ$0f+2bKDCKCGR*cG>+;UTQ2bj z(2r#Od&Ulv*{?U~hq`j8W&8aggxHo<6*$&cDG#k;GS?mLx0^7mda35tz zHTnFA6vB^rczV1Ai8I&XyJX?jiEcQ}n;PYCl~EUPIxF@V%#c7LW`44<>ezAiG>1ff zeOSeCd#PW2z5z+<4Y?Qc#tb&+uH++5^G@!BaaDeVN8x=3ZB{R=Z5e+zf&13+nz{l% z{{#>B^OaIK}1Xh z;}?)W)sfwuf~?Ov1!oiQ-@WVG>D#(JL4Ob-h*l`y&hBY*!EkULKFdt9+VGJ?E=r85 zl*~dE)e4&l8Fdq`I@T2BAme(u7_)}y$TNu^lWWK-M8UQ(ZuBcA(qHG3; z&7bO_w9Cp!REZ3VB`&kfYOCmrNQxu7pbLoFkf)9Jkas&36ZnTBL?~cDug+T3bw?o! z$U-GUnOTkujjaB8vxcenWsZ4UrH*vMmACDj!95aG?gE5-g<6v8X9%kXThF|rP(0eu za*9aK6%^Qu4oyr(1t4hqmPX~~L7tB(;C{DH&MWDzUG+6I(;TGeM)jR#hK~O13LRwk zRc2;#m|qsRADyxC<6XC8u+lvVXoH+-HNTQXImy0_oM&D=ngI3OP?c>&k8&P2iV%hg zq{#n%P=0$dYJ2o$clJWqpVH&Q;S5Hv`T0-)mU2aa$XL#RH`0~|_g zmmfHkP7#d=iuiU1lL&5T+egS~-01WrWiiA=({_yWBnY@x5eX}`?y?3Xdic;`1dn5T zxTwLw{;Qt1MSWowZ}r+U?8Q+R46Avz>o>^}4zhvZaa_*Jd(2A!dP8ah=_*lh!W#a~ zNUm{^sD#HbDq!m*EK}(GzVn4N2GeNpEp8Z<_tctC_id9X=Irqhb_{b^H;~}qwZI&F z3t^MPXp4BuDv9@1Kr3*u zZ|&i`IKW!_Rv5(CaTJBndmX9B{YL8HJ2}u)`_>#J_-m{T-xpj%|2|{xmnVF#+X3=* zY*5{hDkk6M{+!Ved>d}mD@q^#{3qo9ZYb-+75cj*gH%I+d=}E+qSCK>vj4p z81UxB7>Gz}5QU^Pv-AJ*EHMW3g`EwB^^}ps>1E2$#r*H_{O{u)J@@1m$?Pu=va`3n z?so1N_WbU8U+4Nb|AN$Gv|%%33+!xpvv3iSLv&=qIUrD|3^*|rn7cNTWHgpaH0mTS zbXS-J>ZVOG~>BOwxVSa1sk6ivguYJD`$YgKkB!awl#vZ1NenaIidf zIo;H>3%L>R^l(kGI`c9&1a9H-s~68yw>3t6~N-Bv<9hyv4@0XlT|13}n_wh4#^(`bgWSiUFD z?SO{pz~eEqAvU|UZ-MPN$ZoAzAm@B5l}5B&MB(X&#FQ{BiwixOTe9@pn>F;%(9zOZ zly7ELHP0wS+Ikfr4P>I383O6E%8Ps6HYh5VLs3+bL1$J`TkTm6$wnI&{gh;r(^g9_ zB1RO-zhYoFDSl^oIQ*3Sm`H4%TTjHtuLbN&=j+P%iuVlxfEi zjsZUV9XdHY8m9muB8q5Vz z(`L%J6y+JTwbc>-nW(k@1!b!V8X7{S8M4^jErN(9CY}WtZ%l(hygPSA0+WuRy2zYP z{I1rh;dEB2eq9TUxCz{Gyr5B`eQAc=V{W%c+@W5W-mHRf!`2j21`y@SR^7Oz6_2Pt zkOomwUO=FaWS0^zE_8fOUJ%bwuxpLG@_{*8@bC&b7t2Op`l< z@kNX+GMUc*Zm2{Mv|>~c3<+pti9iF4V#K8sFm1soxJDi@ z0hJgP6;T1hrbc}rAns8Ko;#S9v5&XknRCva_O>&b{J*(Da_#Ad?20`5$%Xl&Puge2 zx?l9eH%e}NIwyYKT%Sue)L;7I7JYB)tpVNP7pm4j0n6@>Y|3y<8rov)IM#WzE@P_p zpPF3p<9y7UBK}GHof5CwW07klGghQ%{IeT#5013G-@n^&IFHZTJJ6g~ zCL1d0jcUJO-+8y)#+Wl0=`qCJo^!~ia8$-;rOBE~#*_zRZ*s~5n>IEYEtin@n6TMCEC;3v*irJ77~dTlkH+Ea~ni&gW~z zEBWCpC22aJfc1md!}q~j@)~H{%|IZpVtGYMh}wWjmPAVGFG{e*)g0Ukf*24y3)BXV zL{F7d(CXNXPzVFQlu~e}UL~fsmSnqLDoUS5FIMR1VZnVc3TinGDcHznFA6zTs<73? z4WUqG_@f*^v&jR_Q>a63^$bI30RuiF&nnl+1=px4kSzi_XB+AxOARqt@H;ZXlCce# zxlDYVFRiA{;DaYx(}XclB2S^eT1Q#1;p=9y6{`}J_sm<1Th)5PG zzzBlA<6+TFhl2c=Jl_@yJ}518aXJd2YFCAVu-7TMwT$KZefT7 zs5NxjtWvoM1u)bqHBp$PBs0RBf))u;m?bp>hDT6vTw&Lr!dBTtgj5XtcKJWphk_H; zeH09+T|vQZQ8Efz6lS0!cG`T`QE*MzYzhh@C0zhrg|>NSMAtY9%Huc+TF>Ppkl@@zX1imQDFMlS23i7E;Qs+kyyrF{7O&UZxN+ z-QgiSOj1$l30gw2$s1etFkp1{tI8Eq=&i{Q(-jkZqNBkxHjo*)Mn|Eg=J}ZZ*M!@$ m8X&e#V;O~v<{(@8u;?|riGH1;*CyBcIM_}B>Hc%VBjPV`^lBFX diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties index 1af9e093..e1adfb49 100644 --- a/client/android/gradle/wrapper/gradle-wrapper.properties +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/client/android/gradlew b/client/android/gradlew index 1aa94a42..f5feea6d 100755 --- a/client/android/gradlew +++ b/client/android/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/client/android/gradlew.bat b/client/android/gradlew.bat index 93e3f59f..9d21a218 100644 --- a/client/android/gradlew.bat +++ b/client/android/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/client/android/qt/build.gradle.kts b/client/android/qt/build.gradle.kts index 139adf4f..6b1d3fd1 100644 --- a/client/android/qt/build.gradle.kts +++ b/client/android/qt/build.gradle.kts @@ -21,5 +21,5 @@ android { } dependencies { - implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) + api(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) } diff --git a/client/android/res/values/styles.xml b/client/android/res/values/styles.xml index 9f4201f8..bc67beb9 100644 --- a/client/android/res/values/styles.xml +++ b/client/android/res/values/styles.xml @@ -1,6 +1,9 @@ + #FF0E0E11 diff --git a/client/android/settings.gradle.kts b/client/android/settings.gradle.kts index 5cfc8314..68426ec8 100644 --- a/client/android/settings.gradle.kts +++ b/client/android/settings.gradle.kts @@ -22,7 +22,7 @@ dependencyResolutionManagement { includeBuild("./gradle/plugins") plugins { - id("com.android.settings") version "8.2.0" + id("com.android.settings") version "8.5.2" id("settings-property-delegate") } diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 8a78750b..9d1c31cb 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -158,6 +158,10 @@ class AmneziaActivity : QtActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "Create Amnezia activity: $intent") + window.apply { + addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + statusBarColor = getColor(R.color.black) + } mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) val proto = mainScope.async(Dispatchers.IO) { VpnStateStore.getVpnState().vpnProto @@ -610,6 +614,14 @@ class AmneziaActivity : QtActivity() { } } + @Suppress("unused") + fun setNavigationBarColor(color: Int) { + Log.v(TAG, "Change navigation bar color: ${"#%08X".format(color)}") + mainScope.launch { + window.navigationBarColor = color + } + } + @Suppress("unused") fun minimizeApp() { Log.v(TAG, "Minimize application") @@ -684,6 +696,17 @@ class AmneziaActivity : QtActivity() { .show() } + @Suppress("unused") + fun requestAuthentication() { + Log.v(TAG, "Request authentication") + mainScope.launch { + qtInitialized.await() + Intent(this@AmneziaActivity, AuthActivity::class.java).also { + startActivity(it) + } + } + } + /** * Utils methods */ diff --git a/client/android/src/org/amnezia/vpn/AuthActivity.kt b/client/android/src/org/amnezia/vpn/AuthActivity.kt new file mode 100644 index 00000000..2593315c --- /dev/null +++ b/client/android/src/org/amnezia/vpn/AuthActivity.kt @@ -0,0 +1,97 @@ +package org.amnezia.vpn + +import android.os.Build +import android.os.Bundle +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.AuthenticationResult +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import org.amnezia.vpn.qt.QtAndroidController +import org.amnezia.vpn.util.Log + +private const val TAG = "AuthActivity" + +private const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL + +class AuthActivity : FragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val biometricManager = BiometricManager.from(applicationContext) + when (biometricManager.canAuthenticate(AUTHENTICATORS)) { + BiometricManager.BIOMETRIC_SUCCESS -> { + showBiometricPrompt(biometricManager) + return + } + + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { + Log.w(TAG, "Unknown biometric status") + showBiometricPrompt(biometricManager) + return + } + + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { + Log.e(TAG, "The specified options are incompatible with the current Android " + + "version ${Build.VERSION.SDK_INT}") + } + + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + Log.w(TAG, "The hardware is unavailable") + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + Log.w(TAG, "No biometric or device credential is enrolled") + } + + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { + Log.w(TAG, "There is no suitable hardware") + } + + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { + Log.w(TAG, "A security vulnerability has been discovered with one or " + + "more hardware sensors") + } + } + QtAndroidController.onAuthResult(true) + finish() + } + + private fun showBiometricPrompt(biometricManager: BiometricManager) { + val executor = ContextCompat.getMainExecutor(applicationContext) + val biometricPrompt = BiometricPrompt(this, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.d(TAG, "Authentication succeeded") + QtAndroidController.onAuthResult(true) + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.w(TAG, "Authentication failed") + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Log.e(TAG, "Authentication error $errorCode: $errString") + QtAndroidController.onAuthResult(false) + finish() + } + }) + + + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(AUTHENTICATORS) + .setTitle("AmneziaVPN") + .setSubtitle(biometricManager.getStrings(AUTHENTICATORS)?.promptMessage) + .build() + + biometricPrompt.authenticate(promptInfo) + } +} diff --git a/client/android/src/org/amnezia/vpn/AuthHelper.java b/client/android/src/org/amnezia/vpn/AuthHelper.java deleted file mode 100644 index 940d03c2..00000000 --- a/client/android/src/org/amnezia/vpn/AuthHelper.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.amnezia.vpn; - -import android.content.Context; -import android.app.KeyguardManager; -import android.content.Intent; -import org.qtproject.qt.android.bindings.QtActivity; - - -import static android.content.Context.KEYGUARD_SERVICE; - -public class AuthHelper extends QtActivity { - - static final String TAG = "AuthHelper"; - - public static Intent getAuthIntent(Context context) { - KeyguardManager mKeyguardManager = (KeyguardManager)context.getSystemService(KEYGUARD_SERVICE); - if (mKeyguardManager.isDeviceSecure()) { - return mKeyguardManager.createConfirmDeviceCredentialIntent(null, null); - } else { - return null; - } - } - -} diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index cae7ab75..9faa30d0 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -33,10 +33,10 @@ class ImportConfigActivity : ComponentActivity() { intent?.let(::readConfig) } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.d(TAG, "onNewIntent: $intent") - intent?.let(::readConfig) + intent.let(::readConfig) } private fun readConfig(intent: Intent) { diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index e382b080..4af138a2 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -25,5 +25,7 @@ object QtAndroidController { external fun onConfigImported(data: String) + external fun onAuthResult(result: Boolean) + external fun decodeQrCode(data: String): Boolean } \ No newline at end of file diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index 26d23215..b71bf393 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -88,7 +88,7 @@ class NetworkState( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val numberAttempts = 3 + val numberAttempts = 300 var attemptCount = 0 while(true) { try { diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 13c357bd..c96d9ab8 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -27,7 +27,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h ) @@ -35,7 +34,6 @@ set(HEADERS ${HEADERS} set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp ) diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index c9ee3cfd..2790eb1b 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -98,6 +98,7 @@ bool AndroidController::initialize() {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)}, + {"onAuthResult", "(Z)V", reinterpret_cast(onAuthResult)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)} }; @@ -210,6 +211,11 @@ void AndroidController::setScreenshotsEnabled(bool enabled) callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled); } +void AndroidController::setNavigationBarColor(unsigned int color) +{ + callActivityMethod("setNavigationBarColor", "(I)V", color); +} + void AndroidController::minimizeApp() { callActivityMethod("minimizeApp", "()V"); @@ -265,6 +271,22 @@ void AndroidController::requestNotificationPermission() callActivityMethod("requestNotificationPermission", "()V"); } +bool AndroidController::requestAuthentication() +{ + QEventLoop wait; + bool result; + connect(this, &AndroidController::authenticationResult, this, + [&result, &wait](const bool &authResult){ + qDebug() << "Android authentication result:" << authResult; + result = authResult; + wait.quit(); + }, + static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); + callActivityMethod("requestAuthentication", "()V"); + wait.exec(); + return result; +} + // Moving log processing to the Android side jclass AndroidController::log; jmethodID AndroidController::logDebug; @@ -462,6 +484,14 @@ void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data)); } +// static +void AndroidController::onAuthResult(JNIEnv *env, jobject thiz, jboolean result) +{ + Q_UNUSED(thiz); + + emit AndroidController::instance()->authenticationResult(result); +} + // static bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data) { diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 1041c31f..759c9c3f 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -41,11 +41,13 @@ public: void exportLogsFile(const QString &fileName); void clearLogs(); void setScreenshotsEnabled(bool enabled); + void setNavigationBarColor(unsigned int color); void minimizeApp(); QJsonArray getAppList(); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); bool isNotificationPermissionGranted(); void requestNotificationPermission(); + bool requestAuthentication(); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); @@ -63,6 +65,7 @@ signals: void configImported(QString config); void importConfigFromOutside(QString config); void initConnectionState(Vpn::ConnectionState state); + void authenticationResult(bool result); private: bool isWaitingStatus = true; @@ -89,6 +92,7 @@ private: static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); + static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); template diff --git a/client/platforms/android/authResultReceiver.cpp b/client/platforms/android/authResultReceiver.cpp deleted file mode 100644 index 21e838a2..00000000 --- a/client/platforms/android/authResultReceiver.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "authResultReceiver.h" - -AuthResultReceiver::AuthResultReceiver(QSharedPointer ¬ifier) : m_notifier(notifier) -{ -} - -void AuthResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) -{ - qDebug() << "receiverRequestCode" << receiverRequestCode << "resultCode" << resultCode; - - if (resultCode == -1) { // ResultOK - emit m_notifier->authSuccessful(); - } else { - emit m_notifier->authFailed(); - } -} diff --git a/client/platforms/android/authResultReceiver.h b/client/platforms/android/authResultReceiver.h deleted file mode 100644 index 9a88dcf5..00000000 --- a/client/platforms/android/authResultReceiver.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef AUTHRESULTRECEIVER_H -#define AUTHRESULTRECEIVER_H - -#include - -#include - -class AuthResultNotifier : public QObject -{ - Q_OBJECT - -public: - AuthResultNotifier(QObject *parent = nullptr) : QObject(parent) {}; - -signals: - void authFailed(); - void authSuccessful(); -}; - -/* Auth result handler for Android */ -class AuthResultReceiver final : public QAndroidActivityResultReceiver -{ -public: - AuthResultReceiver(QSharedPointer ¬ifier); - - void handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) override; - -private: - QSharedPointer m_notifier; -}; - -#endif // AUTHRESULTRECEIVER_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 20c32409..2690b5b1 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -10,9 +10,6 @@ #include "core/controllers/vpnConfigurationController.h" #include "systemController.h" -#ifdef Q_OS_ANDROID - #include "platforms/android/android_utils.h" -#endif #include "qrcodegen.hpp" ExportController::ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, @@ -24,12 +21,6 @@ ExportController::ExportController(const QSharedPointer &serversMo m_clientManagementModel(clientManagementModel), m_settings(settings) { -#ifdef Q_OS_ANDROID - m_authResultNotifier.reset(new AuthResultNotifier); - m_authResultReceiver.reset(new AuthResultReceiver(m_authResultNotifier)); - connect(m_authResultNotifier.get(), &AuthResultNotifier::authFailed, this, [this]() { emit exportErrorOccurred(tr("Access error!")); }); - connect(m_authResultNotifier.get(), &AuthResultNotifier::authSuccessful, this, &ExportController::generateFullAccessConfig); -#endif } void ExportController::generateFullAccessConfig() @@ -63,26 +54,6 @@ void ExportController::generateFullAccessConfig() emit exportConfigChanged(); } -#if defined(Q_OS_ANDROID) -void ExportController::generateFullAccessConfigAndroid() -{ - /* We use builtin keyguard for ssh key export protection on Android */ - QJniObject activity = AndroidUtils::getActivity(); - auto appContext = activity.callObjectMethod("getApplicationContext", "()Landroid/content/Context;"); - if (appContext.isValid()) { - auto intent = QJniObject::callStaticObjectMethod("org/amnezia/vpn/AuthHelper", "getAuthIntent", - "(Landroid/content/Context;)Landroid/content/Intent;", appContext.object()); - if (intent.isValid()) { - if (intent.object() != nullptr) { - QtAndroidPrivate::startActivity(intent.object(), 1, m_authResultReceiver.get()); - } - } else { - generateFullAccessConfig(); - } - } -} -#endif - void ExportController::generateConnectionConfig(const QString &clientName) { clearPreviousConfig(); diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index 023f22cf..b031ea39 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -6,9 +6,6 @@ #include "ui/models/clientManagementModel.h" #include "ui/models/containers_model.h" #include "ui/models/servers_model.h" -#ifdef Q_OS_ANDROID - #include "platforms/android/authResultReceiver.h" -#endif class ExportController : public QObject { @@ -25,9 +22,6 @@ public: public slots: void generateFullAccessConfig(); -#if defined(Q_OS_ANDROID) - void generateFullAccessConfigAndroid(); -#endif void generateConnectionConfig(const QString &clientName); void generateOpenVpnConfig(const QString &clientName); void generateWireGuardConfig(const QString &clientName); @@ -74,11 +68,6 @@ private: QString m_config; QString m_nativeConfigString; QList m_qrCodes; - -#ifdef Q_OS_ANDROID - QSharedPointer m_authResultNotifier; - QSharedPointer m_authResultReceiver; -#endif }; #endif // EXPORTCONTROLLER_H diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 3e5e5cc3..9daca272 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -10,8 +10,6 @@ #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" - #include "platforms/android/android_utils.h" - #include #endif #if defined Q_OS_MAC #include "ui/macos_util.h" @@ -22,18 +20,8 @@ PageController::PageController(const QSharedPointer &serversModel, : QObject(parent), m_serversModel(serversModel), m_settings(settings) { #ifdef Q_OS_ANDROID - // Change color of navigation and status bar's auto initialPageNavigationBarColor = getInitialPageNavigationBarColor(); - AndroidUtils::runOnAndroidThreadSync([&initialPageNavigationBarColor]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - window.callMethod("addFlags", "(I)V", 0x80000000); - window.callMethod("clearFlags", "(I)V", 0x04000000); - window.callMethod("setStatusBarColor", "(I)V", 0xFF0E0E11); - window.callMethod("setNavigationBarColor", "(I)V", initialPageNavigationBarColor); - } - }); + AndroidController::instance()->setNavigationBarColor(initialPageNavigationBarColor); #endif #if defined Q_OS_MACX @@ -115,14 +103,7 @@ unsigned int PageController::getInitialPageNavigationBarColor() void PageController::updateNavigationBarColor(const int color) { #ifdef Q_OS_ANDROID - // Change color of navigation bar - AndroidUtils::runOnAndroidThreadSync([&color]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - window.callMethod("setNavigationBarColor", "(I)V", color); - } - }); + AndroidController::instance()->setNavigationBarColor(color); #endif } diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 93fd8971..911bcb16 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -285,3 +285,12 @@ QString SettingsController::getGatewayEndpoint() { return m_settings->getGatewayEndpoint(); } + +bool SettingsController::isOnTv() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->isOnTv(); +#else + return false; +#endif +} \ No newline at end of file diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index a18888a9..89de1bf8 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -82,6 +82,8 @@ public slots: void setGatewayEndpoint(const QString &endpoint); QString getGatewayEndpoint(); + bool isOnTv(); + signals: void primaryDnsChanged(); void secondaryDnsChanged(); diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index e6a9a28e..4598bff1 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -125,3 +125,12 @@ void SystemController::setQmlRoot(QObject *qmlRoot) { m_qmlRoot = qmlRoot; } + +bool SystemController::isAuthenticated() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->requestAuthentication(); +#else + return true; +#endif +} diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 274df234..d2ee6f63 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -19,6 +19,7 @@ public slots: void setQmlRoot(QObject *qmlRoot); + bool isAuthenticated(); signals: void fileDialogClosed(const bool isAccepted); diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index cb706158..fa18703b 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -14,6 +14,7 @@ Button { property string defaultButtonColor: AmneziaStyle.color.paleGray property string progressButtonColor: AmneziaStyle.color.paleGray property string connectedButtonColor: AmneziaStyle.color.goldenApricot + property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv()) implicitWidth: 190 implicitHeight: 190 @@ -50,14 +51,14 @@ Button { verticalOffset: 0 radius: 10 samples: 25 - color: root.activeFocus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.goldenApricot + color: root.buttonActiveFocus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.goldenApricot source: backgroundCircle } ShapePath { fillColor: AmneziaStyle.color.transparent strokeColor: AmneziaStyle.color.paleGray - strokeWidth: root.activeFocus ? 1 : 0 + strokeWidth: root.buttonActiveFocus ? 1 : 0 capStyle: ShapePath.RoundCap PathAngleArc { @@ -81,14 +82,14 @@ Button { return defaultButtonColor } } - strokeWidth: root.activeFocus ? 2 : 3 + strokeWidth: root.buttonActiveFocus ? 2 : 3 capStyle: ShapePath.RoundCap PathAngleArc { centerX: backgroundCircle.width / 2 centerY: backgroundCircle.height / 2 - radiusX: 93 - (root.activeFocus ? 2 : 0) - radiusY: 93 - (root.activeFocus ? 2 : 0) + radiusX: 93 - (root.buttonActiveFocus ? 2 : 0) + radiusY: 93 - (root.buttonActiveFocus ? 2 : 0) startAngle: 0 sweepAngle: 360 } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 4807c030..2a565230 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -140,22 +140,23 @@ PageType { Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { + PageController.showBusyIndicator(true) + + if (Qt.platform.os === "android" && !SystemController.isAuthenticated()) { + PageController.showBusyIndicator(false) + ExportController.exportErrorOccurred(qsTr("Access error!")) + return + } else { + ExportController.generateFullAccessConfig() + } + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text shareConnectionDrawer.open() - shareConnectionDrawer.contentVisible = false - PageController.showBusyIndicator(true) - - if (Qt.platform.os === "android") { - ExportController.generateFullAccessConfigAndroid(); - } else { - ExportController.generateFullAccessConfig(); - } + shareConnectionDrawer.contentVisible = true PageController.showBusyIndicator(false) - - shareConnectionDrawer.contentVisible = true } } } From 918be16372c0a41f783060a6bf58effae9038ed6 Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Sep 2024 16:27:29 +0400 Subject: [PATCH 4/7] feature: added isAvailable flag support (#1032) * feature: added isAvailable flag support * added the option to switch to dev env --- client/CMakeLists.txt | 3 +++ client/core/controllers/apiController.cpp | 21 +++++++++++-------- client/core/controllers/apiController.h | 3 ++- client/settings.cpp | 15 +++++++++++++ client/settings.h | 4 ++++ client/ui/controllers/installController.cpp | 8 +++---- client/ui/controllers/settingsController.cpp | 19 ++++++++++++++++- client/ui/controllers/settingsController.h | 4 ++++ client/ui/models/apiServicesModel.cpp | 19 +++++++++++++++-- client/ui/models/apiServicesModel.h | 1 + client/ui/qml/Controls2/CardWithIconsType.qml | 1 + client/ui/qml/Pages2/PageDevMenu.qml | 15 +++++++++++++ .../Pages2/PageSetupWizardApiServicesList.qml | 6 ++++-- .../Pages2/PageSetupWizardConfigSource.qml | 2 -- 14 files changed, 100 insertions(+), 21 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 327a83f6..a13afb5f 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -27,6 +27,9 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") +add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") +add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}") + if(IOS) set(PACKAGES ${PACKAGES} Multimedia) endif() diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 8e5f8ed5..3f8684e0 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -9,8 +9,8 @@ #include "QRsa.h" #include "amnezia_application.h" -#include "core/enums/apiEnums.h" #include "configurators/wireguard_configurator.h" +#include "core/enums/apiEnums.h" #include "version.h" namespace @@ -42,7 +42,7 @@ namespace constexpr char keyPayload[] = "key_payload"; } - const QStringList proxyStorageUrl = {""}; + const QStringList proxyStorageUrl = { "" }; ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { @@ -65,7 +65,8 @@ namespace } } -ApiController::ApiController(const QString &gatewayEndpoint, QObject *parent) : QObject(parent), m_gatewayEndpoint(gatewayEndpoint) +ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent) + : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment) { } @@ -143,7 +144,7 @@ QStringList ApiController::getProxyUrls() QEventLoop wait; QList sslErrors; - QNetworkReply* reply; + QNetworkReply *reply; for (const auto &proxyStorageUrl : proxyStorageUrl) { request.setUrl(proxyStorageUrl); @@ -281,7 +282,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); - QNetworkReply* reply; + QNetworkReply *reply; reply = amnApp->manager()->get(request); QEventLoop wait; @@ -300,7 +301,8 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + if (reply->error() != QNetworkReply::NetworkError::TimeoutError + && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { break; } reply->deleteLater(); @@ -355,7 +357,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co EVP_PKEY *publicKey = nullptr; try { - QByteArray key = PROD_AGW_PUBLIC_KEY; + QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QSimpleCrypto::QRsa rsa; publicKey = rsa.getPublicKeyFromByteArray(key); } catch (...) { @@ -375,7 +377,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply* reply = manager.post(request, QJsonDocument(requestBody).toJson()); + QNetworkReply *reply = manager.post(request, QJsonDocument(requestBody).toJson()); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -395,7 +397,8 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + if (reply->error() != QNetworkReply::NetworkError::TimeoutError + && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { break; } reply->deleteLater(); diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index 6cfde983..a094233b 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -14,7 +14,7 @@ class ApiController : public QObject Q_OBJECT public: - explicit ApiController(const QString &gatewayEndpoint, QObject *parent = nullptr); + explicit ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent = nullptr); public slots: void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); @@ -44,6 +44,7 @@ private: QString m_gatewayEndpoint; QStringList m_proxyUrls; + bool m_isDevEnvironment; }; #endif // APICONTROLLER_H diff --git a/client/settings.cpp b/client/settings.cpp index 490ede52..8faf00c2 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -519,7 +519,22 @@ void Settings::setGatewayEndpoint(const QString &endpoint) m_gatewayEndpoint = endpoint; } +void Settings::setDevGatewayEndpoint() +{ + m_gatewayEndpoint = DEV_AGW_ENDPOINT; +} + QString Settings::getGatewayEndpoint() { return m_gatewayEndpoint; } + +bool Settings::isDevGatewayEnv() +{ + return m_isDevGatewayEnv; +} + +void Settings::toggleDevGatewayEnv(bool enabled) +{ + m_isDevGatewayEnv = enabled; +} diff --git a/client/settings.h b/client/settings.h index 55a3d057..c0ab0559 100644 --- a/client/settings.h +++ b/client/settings.h @@ -217,7 +217,10 @@ public: void resetGatewayEndpoint(); void setGatewayEndpoint(const QString &endpoint); + void setDevGatewayEndpoint(); QString getGatewayEndpoint(); + bool isDevGatewayEnv(); + void toggleDevGatewayEnv(bool enabled); signals: void saveLogsChanged(bool enabled); @@ -234,6 +237,7 @@ private: mutable SecureQSettings m_settings; QString m_gatewayEndpoint; + bool m_isDevGatewayEnv; }; #endif // SETTINGS_H diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 66e04520..c6f17057 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -799,7 +799,7 @@ void InstallController::addEmptyServer() bool InstallController::fillAvailableServices() { - ApiController apiController(m_settings->getGatewayEndpoint()); + ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); QByteArray responseBody; ErrorCode errorCode = apiController.getServicesList(responseBody); @@ -821,7 +821,7 @@ bool InstallController::installServiceFromApi() return false; } - ApiController apiController(m_settings->getGatewayEndpoint()); + ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); QJsonObject serverConfig; ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(), @@ -849,7 +849,7 @@ bool InstallController::installServiceFromApi() bool InstallController::updateServiceFromApi(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig) { - ApiController apiController(m_settings->getGatewayEndpoint()); + ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); @@ -885,7 +885,7 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin void InstallController::updateServiceFromTelegram(const int serverIndex) { - ApiController *apiController = new ApiController(m_settings->getGatewayEndpoint()); + ApiController *apiController = new ApiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); auto serverConfig = m_serversModel->getServerConfig(serverIndex); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 911bcb16..7a95589b 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -283,7 +283,24 @@ void SettingsController::setGatewayEndpoint(const QString &endpoint) QString SettingsController::getGatewayEndpoint() { - return m_settings->getGatewayEndpoint(); + return m_settings->isDevGatewayEnv() ? "Dev endpoint" : m_settings->getGatewayEndpoint(); +} + +bool SettingsController::isDevGatewayEnv() +{ + return m_settings->isDevGatewayEnv(); +} + +void SettingsController::toggleDevGatewayEnv(bool enabled) +{ + m_settings->toggleDevGatewayEnv(enabled); + if (enabled) { + m_settings->setDevGatewayEndpoint(); + } else { + m_settings->resetGatewayEndpoint(); + } + emit gatewayEndpointChanged(m_settings->getGatewayEndpoint()); + emit devGatewayEnvChanged(enabled); } bool SettingsController::isOnTv() diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 89de1bf8..d64e63cf 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -27,6 +27,7 @@ public: Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled) Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged) + Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged) public slots: void toggleAmneziaDns(bool enable); @@ -81,6 +82,8 @@ public slots: void resetGatewayEndpoint(); void setGatewayEndpoint(const QString &endpoint); QString getGatewayEndpoint(); + bool isDevGatewayEnv(); + void toggleDevGatewayEnv(bool enabled); bool isOnTv(); @@ -105,6 +108,7 @@ signals: void devModeEnabled(); void gatewayEndpointChanged(const QString &endpoint); + void devGatewayEnvChanged(bool enabled); private: QSharedPointer m_serversModel; diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 3e74d259..2a87bde3 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -25,6 +25,8 @@ namespace constexpr char availableCountries[] = "available_countries"; constexpr char storeEndpoint[] = "store_endpoint"; + + constexpr char isAvailable[] = "is_available"; } namespace serviceType @@ -63,8 +65,12 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); - } else { - return tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); + } else if (serviceType == serviceType::amneziaFree){ + QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); + if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + description += tr("

    Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); + } + return description; } } case ServiceDescriptionRole: { @@ -75,6 +81,14 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const return tr("Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship"); } } + case IsServiceAvailableRole: { + if (serviceType == serviceType::amneziaFree) { + if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + return false; + } + } + return true; + } case SpeedRole: { auto speed = serviceInfo.value(configKey::speed).toString(); return tr("%1 MBit/s").arg(speed); @@ -193,6 +207,7 @@ QHash ApiServicesModel::roleNames() const roles[NameRole] = "name"; roles[CardDescriptionRole] = "cardDescription"; roles[ServiceDescriptionRole] = "serviceDescription"; + roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; roles[WorkPeriodRole] = "workPeriod"; roles[RegionRole] = "region"; diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 64676be6..49918940 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -13,6 +13,7 @@ public: NameRole = Qt::UserRole + 1, CardDescriptionRole, ServiceDescriptionRole, + IsServiceAvailableRole, SpeedRole, WorkPeriodRole, RegionRole, diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index 8630434b..fea65116 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -79,6 +79,7 @@ Button { visible: text !== "" color: AmneziaStyle.color.mutedGray + textFormat: Text.RichText Layout.fillWidth: true Layout.rightMargin: 16 diff --git a/client/ui/qml/Pages2/PageDevMenu.qml b/client/ui/qml/Pages2/PageDevMenu.qml index af6f773a..5da40eff 100644 --- a/client/ui/qml/Pages2/PageDevMenu.qml +++ b/client/ui/qml/Pages2/PageDevMenu.qml @@ -89,6 +89,21 @@ PageType { // KeyNavigation.tab: saveButton } + + SwitcherType { + id: switcher + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.topMargin: 16 + + text: qsTr("Dev gateway environment") + checked: SettingsController.isDevGatewayEnv + onToggled: function() { + SettingsController.isDevGatewayEnv = checked + } + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index cb79f19e..85a50393 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -88,8 +88,10 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" onClicked: { - ApiServicesModel.setServiceIndex(index) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 3febca4c..3c56b52e 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -119,8 +119,6 @@ PageType { CardWithIconsType { id: apiInstalling - visible: false - Layout.fillWidth: true Layout.rightMargin: 16 Layout.leftMargin: 16 From 9cab51fb0025ec03c30b596ef27328da711611ac Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Sep 2024 20:53:44 +0400 Subject: [PATCH 5/7] added open service logs to logs page (#951) * added open service logs to logs page * redesign of log saving buttons * hide service logs buttons for mobile platforms * refactoring: moved logger to common folder * feature: added the ability to enable logs to the start screen --- client/CMakeLists.txt | 4 +- client/amnezia_application.cpp | 2 +- client/core/controllers/serverController.cpp | 2 - client/logger.h | 107 ------- client/settings.cpp | 2 +- client/ui/controllers/settingsController.cpp | 18 +- client/ui/controllers/settingsController.h | 2 + client/ui/property_helper.h | 27 -- .../ui/qml/Controls2/LabelWithButtonType.qml | 9 +- client/ui/qml/Controls2/SwitcherType.qml | 3 +- client/ui/qml/Pages2/PageSettingsLogging.qml | 299 +++++++++++------- .../Pages2/PageSetupWizardConfigSource.qml | 45 +++ client/ui/qml/Pages2/PageStart.qml | 8 + client/utilities.cpp | 16 - client/utilities.h | 1 - {client => common/logger}/logger.cpp | 188 ++++++----- common/logger/logger.h | 114 +++++++ ipc/ipcserver.cpp | 88 ++---- service/server/CMakeLists.txt | 5 +- service/server/logger.cpp | 185 ----------- service/server/logger.h | 83 ----- service/server/main.cpp | 2 +- 22 files changed, 519 insertions(+), 691 deletions(-) delete mode 100644 client/logger.h delete mode 100644 client/ui/property_helper.h rename {client => common/logger}/logger.cpp (56%) create mode 100644 common/logger/logger.h delete mode 100644 service/server/logger.cpp delete mode 100644 service/server/logger.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a13afb5f..4d0c1c2a 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -113,6 +113,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake) include_directories( ${CMAKE_CURRENT_LIST_DIR}/../ipc + ${CMAKE_CURRENT_LIST_DIR}/../common/logger ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) @@ -134,7 +135,6 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h ${CMAKE_CURRENT_LIST_DIR}/ui/pages.h - ${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h ${CMAKE_CURRENT_BINARY_DIR}/version.h @@ -143,6 +143,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h ${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h ${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h + ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h ) # Mozilla headres @@ -193,6 +194,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp + ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp ) # Mozilla sources diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index b8ce5b00..526b9fa9 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -164,7 +164,7 @@ void AmneziaApplication::init() bool enabled = m_settings->isSaveLogs(); #ifndef Q_OS_ANDROID if (enabled) { - if (!Logger::init()) { + if (!Logger::init(false)) { qWarning() << "Initialization of debug subsystem failed"; } } diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 233d66d4..9a745e3d 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -83,7 +83,6 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr } qDebug().noquote() << lineToExec; - Logger::appendSshLog("Run command:" + lineToExec); error = m_sshClient.executeCommand(lineToExec, cbReadStdOut, cbReadStdErr); if (error != ErrorCode::NoError) { @@ -100,7 +99,6 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti const std::function &cbReadStdErr) { QString fileName = "/opt/amnezia/" + Utils::getRandomString(16) + ".sh"; - Logger::appendSshLog("Run container script for " + ContainerProps::containerToString(container) + ":\n" + script); ErrorCode e = uploadTextFileToContainer(container, credentials, script, fileName); if (e) diff --git a/client/logger.h b/client/logger.h deleted file mode 100644 index 0dcbd35c..00000000 --- a/client/logger.h +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include -#include - -#include "ui/property_helper.h" - -#include "mozilla/shared/loglevel.h" - -class Logger : public QObject -{ - Q_OBJECT - AUTO_PROPERTY(QString, sshLog) - AUTO_PROPERTY(QString, allLog) - -public: - static Logger& Instance(); - - static void appendSshLog(const QString &log); - static void appendAllLog(const QString &log); - - - static bool init(); - static void deInit(); - static bool setServiceLogsEnabled(bool enabled); - static bool openLogsFolder(); - static bool openServiceLogsFolder(); - static QString appLogFileNamePath(); - static void clearLogs(); - static void clearServiceLogs(); - static void cleanUp(); - - static QString userLogsFilePath(); - static QString getLogFile(); - - // compat with Mozilla logger - Logger(const QString &className) { m_className = className; } - const QString& className() const { return m_className; } - - class Log { - public: - Log(Logger* logger, LogLevel level); - ~Log(); - - Log& operator<<(uint64_t t); - Log& operator<<(const char* t); - Log& operator<<(const QString& t); - Log& operator<<(const QStringList& t); - Log& operator<<(const QByteArray& t); - Log& operator<<(const QJsonObject& t); - Log& operator<<(QTextStreamFunction t); - Log& operator<<(const void* t); - - // Q_ENUM - template - typename std::enable_if::Value, Log&>::type - operator<<(T t) { - const QMetaObject* meta = qt_getEnumMetaObject(t); - const char* name = qt_getEnumName(t); - addMetaEnum(typename QFlags::Int(t), meta, name); - return *this; - } - - private: - void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name); - - Logger* m_logger; - LogLevel m_logLevel; - - struct Data { - Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {} - - QString m_buffer; - QTextStream m_ts; - }; - - Data* m_data; - }; - - Log error(); - Log warning(); - Log info(); - Log debug(); - QString sensitive(const QString& input); - -private: - Logger() {} - Logger(Logger const &) = delete; - Logger& operator= (Logger const&) = delete; - - static QString userLogsDir(); - - static QFile m_file; - static QTextStream m_textStream; - static QString m_logFileName; - - friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - // compat with Mozilla logger - QString m_className; -}; - -#endif // LOGGER_H diff --git a/client/settings.cpp b/client/settings.cpp index 8faf00c2..7a572a13 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -227,7 +227,7 @@ void Settings::setSaveLogs(bool enabled) if (!isSaveLogs()) { Logger::deInit(); } else { - if (!Logger::init()) { + if (!Logger::init(false)) { qWarning() << "Initialization of debug subsystem failed"; } } diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 7a95589b..c3945512 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -88,7 +88,12 @@ void SettingsController::toggleLogging(bool enable) void SettingsController::openLogsFolder() { - Logger::openLogsFolder(); + Logger::openLogsFolder(false); +} + +void SettingsController::openServiceLogsFolder() +{ + Logger::openLogsFolder(true); } void SettingsController::exportLogsFile(const QString &fileName) @@ -100,12 +105,21 @@ void SettingsController::exportLogsFile(const QString &fileName) #endif } +void SettingsController::exportServiceLogsFile(const QString &fileName) +{ +#ifdef Q_OS_ANDROID + AndroidController::instance()->exportLogsFile(fileName); +#else + SystemController::saveFile(fileName, Logger::getServiceLogFile()); +#endif +} + void SettingsController::clearLogs() { #ifdef Q_OS_ANDROID AndroidController::instance()->clearLogs(); #else - Logger::clearLogs(); + Logger::clearLogs(false); Logger::clearServiceLogs(); #endif } diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index d64e63cf..efc18a7d 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -43,7 +43,9 @@ public slots: void toggleLogging(bool enable); void openLogsFolder(); + void openServiceLogsFolder(); void exportLogsFile(const QString &fileName); + void exportServiceLogsFile(const QString &fileName); void clearLogs(); void backupAppConfig(const QString &fileName); diff --git a/client/ui/property_helper.h b/client/ui/property_helper.h deleted file mode 100644 index 927105b3..00000000 --- a/client/ui/property_helper.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef PROPERTY_HELPER_H -#define PROPERTY_HELPER_H - -#include - -#define AUTO_PROPERTY(TYPE, NAME) \ - Q_PROPERTY(TYPE NAME READ NAME WRITE set_ ## NAME NOTIFY NAME ## Changed ) \ - public: \ - TYPE NAME() const { return m_ ## NAME ; } \ - void set_ ## NAME(TYPE value) { \ - if (m_ ## NAME == value) return; \ - m_ ## NAME = value; \ - emit NAME ## Changed(value); \ - } \ - Q_SIGNAL void NAME ## Changed(TYPE value);\ - private: \ - TYPE m_ ## NAME{}; - -#define READONLY_PROPERTY(TYPE, NAME) \ - Q_PROPERTY(TYPE NAME READ NAME CONSTANT ) \ - public: \ - TYPE NAME() const { return m_ ## NAME ; } \ - private: \ - void NAME(TYPE value) {m_ ## NAME = value; } \ - TYPE m_ ## NAME{}; - -#endif // PROPERTY_HELPER_H diff --git a/client/ui/qml/Controls2/LabelWithButtonType.qml b/client/ui/qml/Controls2/LabelWithButtonType.qml index 3b1609f7..41faf108 100644 --- a/client/ui/qml/Controls2/LabelWithButtonType.qml +++ b/client/ui/qml/Controls2/LabelWithButtonType.qml @@ -20,7 +20,8 @@ Item { property string buttonImageSource property string rightImageSource property string leftImageSource - property bool isLeftImageHoverEnabled: true //todo separete this qml file to 3 + property bool isLeftImageHoverEnabled: true + property bool isSmallLeftImage: false property alias rightButton: rightImage property alias eyeButton: eyeImage @@ -114,9 +115,9 @@ Item { visible: leftImageSource ? true : false - Layout.preferredHeight: rightImageSource || !isLeftImageHoverEnabled ? leftImage.implicitHeight : 56 - Layout.preferredWidth: rightImageSource || !isLeftImageHoverEnabled ? leftImage.implicitWidth : 56 - Layout.rightMargin: rightImageSource || !isLeftImageHoverEnabled ? 16 : 0 + Layout.preferredHeight: (rightImageSource || !isLeftImageHoverEnabled || isSmallLeftImage) ? 40 : 56 + Layout.preferredWidth: (rightImageSource || !isLeftImageHoverEnabled || isSmallLeftImage)? 40 : 56 + Layout.rightMargin: isSmallLeftImage ? 8 : (rightImageSource || !isLeftImageHoverEnabled) ? 16 : 0 radius: 12 color: AmneziaStyle.color.transparent diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index 9b2885ea..43c35778 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -102,8 +102,7 @@ Switch { contentItem: ColumnLayout { id: content - anchors.top: parent.top - anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left ListItemTitleType { diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index 3ab0df8a..9abfc453 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -16,18 +16,6 @@ import "../Controls2/TextTypes" PageType { id: root - Connections { - target: SettingsController - - function onLoggingStateChanged() { - if (SettingsController.isLoggingEnabled) { - var message = qsTr("Logging is enabled. Note that logs will be automatically \ -disabled after 14 days, and all log files will be deleted.") - PageController.showNotificationMessage(message) - } - } - } - defaultActiveFocusItem: focusItem Item { @@ -58,13 +46,12 @@ disabled after 14 days, and all log files will be deleted.") anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - spacing: 16 + spacing: 0 HeaderType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 headerText: qsTr("Logging") descriptionText: qsTr("Enabling this function will save application's logs automatically. " + @@ -75,11 +62,13 @@ disabled after 14 days, and all log files will be deleted.") id: switcher Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - text: qsTr("Save logs") + text: qsTr("Enable logs") checked: SettingsController.isLoggingEnabled - KeyNavigation.tab: openFolderButton + //KeyNavigation.tab: openFolderButton onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { SettingsController.isLoggingEnabled = checked @@ -87,132 +76,200 @@ disabled after 14 days, and all log files will be deleted.") } } - RowLayout { + DividerType {} + + LabelWithButtonType { + // id: labelWithButton2 Layout.fillWidth: true + Layout.topMargin: -8 - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: GC.isMobile() ? 0 : root.width / 3 - visible: !GC.isMobile() + text: qsTr("Clear logs") + leftImageSource: "qrc:/images/controls/trash.svg" + isSmallLeftImage: true - ImageButtonType { - id: openFolderButton - Layout.alignment: Qt.AlignHCenter + // KeyNavigation.tab: labelWithButton3 - implicitWidth: 56 - implicitHeight: 56 + clickedFunction: function() { + var headerText = qsTr("Clear logs?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - image: "qrc:/images/controls/folder-open.svg" - KeyNavigation.tab: saveButton - - onClicked: SettingsController.openLogsFolder() - Keys.onReturnPressed: openFolderButton.clicked() - Keys.onEnterPressed: openFolderButton.clicked() + var yesButtonFunction = function() { + PageController.showBusyIndicator(true) + SettingsController.clearLogs() + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } - - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - - text: qsTr("Open folder with logs") - color: AmneziaStyle.color.paleGray - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) - - ImageButtonType { - id: saveButton - Layout.alignment: Qt.AlignHCenter - - implicitWidth: 56 - implicitHeight: 56 - - image: "qrc:/images/controls/save.svg" - KeyNavigation.tab: clearButton - - Keys.onReturnPressed: saveButton.clicked() - Keys.onEnterPressed: saveButton.clicked() - onClicked: { - var fileName = "" - if (GC.isMobile()) { - fileName = "AmneziaVPN.log" - } else { - fileName = SystemController.getFileName(qsTr("Save"), - qsTr("Logs files (*.log)"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN", - true, - ".log") - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - SettingsController.exportLogsFile(fileName) - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs file saved")) - } + var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() } } - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } - text: qsTr("Save logs to file") - color: AmneziaStyle.color.paleGray + ListItemTitleType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Client logs") + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + color: AmneziaStyle.color.mutedGray + text: qsTr("AmneziaVPN logs") + } + + LabelWithButtonType { + // id: labelWithButton2 + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Open logs folder") + leftImageSource: "qrc:/images/controls/folder-open.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + SettingsController.openLogsFolder() + } + } + + DividerType {} + + LabelWithButtonType { + // id: labelWithButton2 + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Export logs") + leftImageSource: "qrc:/images/controls/save.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN", + true, + ".log") + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } } + } - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) + DividerType {} - ImageButtonType { - id: clearButton - Layout.alignment: Qt.AlignHCenter + ListItemTitleType { + visible: !GC.isMobile() - implicitWidth: 56 - implicitHeight: 56 + Layout.fillWidth: true + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - image: "qrc:/images/controls/delete.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) + text: qsTr("Service logs") + } - Keys.onReturnPressed: clearButton.clicked() - Keys.onEnterPressed: clearButton.clicked() - onClicked: function() { - var headerText = qsTr("Clear logs?") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") + ParagraphTextType { + visible: !GC.isMobile() - var yesButtonFunction = function() { - PageController.showBusyIndicator(true) - SettingsController.clearLogs() - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } + color: AmneziaStyle.color.mutedGray + text: qsTr("AmneziaVPN-service logs") + } + + LabelWithButtonType { + // id: labelWithButton2 + + visible: !GC.isMobile() + + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Open logs folder") + leftImageSource: "qrc:/images/controls/folder-open.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + SettingsController.openServiceLogsFolder() + } + } + + DividerType { + visible: !GC.isMobile() + } + + LabelWithButtonType { + // id: labelWithButton2 + + visible: !GC.isMobile() + + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Export logs") + leftImageSource: "qrc:/images/controls/save.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN-service.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service", + true, + ".log") } - - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - - text: qsTr("Clear logs") - color: AmneziaStyle.color.paleGray + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportServiceLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } } } + + DividerType { + visible: !GC.isMobile() + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 3c56b52e..7f7cf9e1 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -55,6 +55,51 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Connection") + + actionButtonImage: PageController.isStartPageVisible() ? "qrc:/images/controls/more-vertical.svg" : "" + actionButtonFunction: function() { + moreActionsDrawer.open() + } + + DrawerType2 { + id: moreActionsDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.35 + + expandedContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 32 + + headerText: qsTr("Settings") + } + + SwitcherType { + id: switcher + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Enable logs") + + checked: SettingsController.isLoggingEnabled + onCheckedChanged: { + if (checked !== SettingsController.isLoggingEnabled) { + SettingsController.isLoggingEnabled = checked + } + } + } + + } + } } ParagraphTextType { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 770347ca..bb6663fb 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -202,6 +202,14 @@ PageType { PageController.showNotificationMessage(qsTr("Settings restored from backup file")) PageController.goToPageHome() } + + function onLoggingStateChanged() { + if (SettingsController.isLoggingEnabled) { + var message = qsTr("Logging is enabled. Note that logs will be automatically" + + "disabled after 14 days, and all log files will be deleted.") + PageController.showNotificationMessage(message) + } + } } StackViewType { diff --git a/client/utilities.cpp b/client/utilities.cpp index a2f3d021..4047365f 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -69,22 +69,6 @@ QString Utils::JsonToString(const QJsonArray &array, QJsonDocument::JsonFormat f return doc.toJson(format); } -QString Utils::systemLogPath() -{ -#ifdef Q_OS_WIN - QStringList locationList = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); - QString primaryLocation = "ProgramData"; - foreach (const QString &location, locationList) { - if (location.contains(primaryLocation)) { - return QString("%1/%2/log").arg(location).arg(APPLICATION_NAME); - } - } - return QString(); -#else - return QString("/var/log/%1").arg(APPLICATION_NAME); -#endif -} - bool Utils::initializePath(const QString &path) { QDir dir; diff --git a/client/utilities.h b/client/utilities.h index b85c5b3b..9bf8c82a 100644 --- a/client/utilities.h +++ b/client/utilities.h @@ -23,7 +23,6 @@ public: static QJsonObject JsonFromString(const QString &string); static QString executable(const QString &baseName, bool absPath); static QString usrExecutable(const QString &baseName); - static QString systemLogPath(); static bool createEmptyFile(const QString &path); static bool initializePath(const QString &path); diff --git a/client/logger.cpp b/common/logger/logger.cpp similarity index 56% rename from client/logger.cpp rename to common/logger/logger.cpp index c76bc698..747590b9 100644 --- a/client/logger.cpp +++ b/common/logger/logger.cpp @@ -4,18 +4,18 @@ #include #include #include -#include #include +#include #include #include #include -#include "version.h" #include "utilities.h" +#include "version.h" #ifdef AMNEZIA_DESKTOP -#include + #include #endif #ifdef Q_OS_IOS @@ -25,8 +25,9 @@ QFile Logger::m_file; QTextStream Logger::m_textStream; QString Logger::m_logFileName = QString("%1.log").arg(APPLICATION_NAME); +QString Logger::m_serviceLogFileName = QString("%1.log").arg(SERVICE_NAME); -void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) +void debugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { if (msg.simplified().isEmpty()) { return; @@ -37,12 +38,12 @@ void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, cons return; } - if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") || msg.startsWith("stale focus object")) { + if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") + || msg.startsWith("stale focus object")) { return; } Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; - Logger::appendAllLog(qFormatLogMessage(type, context, msg)); std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; } @@ -53,36 +54,24 @@ Logger &Logger::Instance() return s; } -void Logger::appendSshLog(const QString &log) +bool Logger::init(bool isServiceLogger) { - QString dt = QDateTime::currentDateTime().toString(); - Instance().m_sshLog.append(dt + ": " + log + "\n"); - emit Instance().sshLogChanged(Instance().sshLog()); -} - -void Logger::appendAllLog(const QString &log) -{ - Instance().m_allLog.append(log + "\n"); - emit Instance().allLogChanged(Instance().allLog()); -} - -bool Logger::init() -{ - qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); - - QString path = userLogsDir(); + QString path = isServiceLogger ? systemLogDir() : userLogsDir(); + QString logFileName = isServiceLogger ? m_serviceLogFileName : m_logFileName ; QDir appDir(path); if (!appDir.mkpath(path)) { return false; } - m_file.setFileName(appDir.filePath(m_logFileName)); + m_file.setFileName(appDir.filePath(logFileName)); if (!m_file.open(QIODevice::Append)) { - qWarning() << "Cannot open log file:" << m_logFileName; + qWarning() << "Cannot open log file:" << logFileName; return false; } + m_file.setTextModeEnabled(true); m_textStream.setDevice(&m_file); + qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); #if !defined(QT_DEBUG) || defined(Q_OS_IOS) qInstallMessageHandler(debugMessageHandler); @@ -99,7 +88,8 @@ void Logger::deInit() m_file.close(); } -bool Logger::setServiceLogsEnabled(bool enabled) { +bool Logger::setServiceLogsEnabled(bool enabled) +{ #ifdef AMNEZIA_DESKTOP IpcClient *m_IpcClient = new IpcClient; @@ -112,8 +102,7 @@ bool Logger::setServiceLogsEnabled(bool enabled) { if (m_IpcClient->Interface()) { m_IpcClient->Interface()->setLogsEnabled(enabled); - } - else { + } else { qWarning() << "Error occurred setting up service logs"; return false; } @@ -127,11 +116,32 @@ QString Logger::userLogsDir() return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/log"; } +QString Logger::systemLogDir() +{ +#ifdef Q_OS_WIN + QStringList locationList = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + QString primaryLocation = "ProgramData"; + foreach (const QString &location, locationList) { + if (location.contains(primaryLocation)) { + return QString("%1/%2/log").arg(location).arg(APPLICATION_NAME); + } + } + return QString(); +#else + return QString("/var/log/%1").arg(APPLICATION_NAME); +#endif +} + QString Logger::userLogsFilePath() { return userLogsDir() + QDir::separator() + m_logFileName; } +QString Logger::serviceLogsFilePath() +{ + return systemLogDir() + QDir::separator() + m_serviceLogFileName; +} + QString Logger::getLogFile() { m_file.flush(); @@ -139,18 +149,32 @@ QString Logger::getLogFile() file.open(QIODevice::ReadOnly); QString qtLog = file.readAll(); - + #ifdef Q_OS_IOS return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); #else return qtLog; #endif - } -bool Logger::openLogsFolder() +QString Logger::getServiceLogFile() { - QString path = userLogsDir(); + m_file.flush(); + QFile file(serviceLogsFilePath()); + + file.open(QIODevice::ReadOnly); + QString qtLog = file.readAll(); + +#ifdef Q_OS_IOS + return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); +#else + return qtLog; +#endif +} + +bool Logger::openLogsFolder(bool isServiceLogger) +{ + QString path = isServiceLogger ? systemLogDir() : userLogsDir(); #ifdef Q_OS_WIN path = "file:///" + path; #endif @@ -161,38 +185,23 @@ bool Logger::openLogsFolder() return true; } -bool Logger::openServiceLogsFolder() -{ - QString path = Utils::systemLogPath(); -#ifdef Q_OS_WIN - path = "file:///" + path; -#endif - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); - return true; -} - -QString Logger::appLogFileNamePath() -{ - return m_file.fileName(); -} - -void Logger::clearLogs() +void Logger::clearLogs(bool isServiceLogger) { bool isLogActive = m_file.isOpen(); m_file.close(); - QFile file(userLogsFilePath()); + QFile file(isServiceLogger ? serviceLogsFilePath() : userLogsFilePath()); file.open(QIODevice::WriteOnly | QIODevice::Truncate); file.resize(0); file.close(); - + #ifdef Q_OS_IOS AmneziaVPN::swiftDeleteLog(); #endif - + if (isLogActive) { - init(); + init(isServiceLogger); } } @@ -210,8 +219,7 @@ void Logger::clearServiceLogs() if (m_IpcClient->Interface()) { m_IpcClient->Interface()->clearLogs(); - } - else { + } else { qWarning() << "Error occurred cleaning up service logs"; } #endif @@ -219,26 +227,41 @@ void Logger::clearServiceLogs() void Logger::cleanUp() { - clearLogs(); + clearLogs(false); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.removeRecursively(); - clearServiceLogs(); + clearLogs(true); } -Logger::Log::Log(Logger* logger, LogLevel logLevel) - : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) {} +Logger::Log::Log(Logger *logger, LogLevel logLevel) : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) +{ +} -Logger::Log::~Log() { +Logger::Log::~Log() +{ qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); delete m_data; } -Logger::Log Logger::error() { return Log(this, LogLevel::Error); } -Logger::Log Logger::warning() { return Log(this, LogLevel::Warning); } -Logger::Log Logger::info() { return Log(this, LogLevel::Info); } -Logger::Log Logger::debug() { return Log(this, LogLevel::Debug); } -QString Logger::sensitive(const QString& input) { +Logger::Log Logger::error() +{ + return Log(this, LogLevel::Error); +} +Logger::Log Logger::warning() +{ + return Log(this, LogLevel::Warning); +} +Logger::Log Logger::info() +{ + return Log(this, LogLevel::Info); +} +Logger::Log Logger::debug() +{ + return Log(this, LogLevel::Debug); +} +QString Logger::sensitive(const QString &input) +{ #ifdef Q_DEBUG return input; #else @@ -247,48 +270,51 @@ QString Logger::sensitive(const QString& input) { #endif } - -#define CREATE_LOG_OP_REF(x) \ -Logger::Log& Logger::Log::operator<<(x t) { \ - m_data->m_ts << t << ' '; \ - return *this; \ -} +#define CREATE_LOG_OP_REF(x) \ + Logger::Log &Logger::Log::operator<<(x t) \ + { \ + m_data->m_ts << t << ' '; \ + return *this; \ + } CREATE_LOG_OP_REF(uint64_t); -CREATE_LOG_OP_REF(const char*); -CREATE_LOG_OP_REF(const QString&); -CREATE_LOG_OP_REF(const QByteArray&); -CREATE_LOG_OP_REF(const void*); +CREATE_LOG_OP_REF(const char *); +CREATE_LOG_OP_REF(const QString &); +CREATE_LOG_OP_REF(const QByteArray &); +CREATE_LOG_OP_REF(const void *); #undef CREATE_LOG_OP_REF -Logger::Log& Logger::Log::operator<<(const QStringList& t) { +Logger::Log &Logger::Log::operator<<(const QStringList &t) +{ m_data->m_ts << '[' << t.join(",") << ']' << ' '; return *this; } -Logger::Log& Logger::Log::operator<<(const QJsonObject& t) { +Logger::Log &Logger::Log::operator<<(const QJsonObject &t) +{ m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; return *this; } -Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { +Logger::Log &Logger::Log::operator<<(QTextStreamFunction t) +{ m_data->m_ts << t; return *this; } -void Logger::Log::addMetaEnum(quint64 value, const QMetaObject* meta, - const char* name) { +void Logger::Log::addMetaEnum(quint64 value, const QMetaObject *meta, const char *name) +{ QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); QString out; QTextStream ts(&out); - if (const char* scope = me.scope()) { + if (const char *scope = me.scope()) { ts << scope << "::"; } - const char* key = me.valueToKey(static_cast(value)); + const char *key = me.valueToKey(static_cast(value)); const bool scoped = me.isScoped(); if (scoped || !key) { ts << me.enumName() << (!key ? "(" : "::"); diff --git a/common/logger/logger.h b/common/logger/logger.h new file mode 100644 index 00000000..7dff7ede --- /dev/null +++ b/common/logger/logger.h @@ -0,0 +1,114 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include +#include +#include + +#include "mozilla/shared/loglevel.h" + +class Logger : public QObject +{ + Q_OBJECT + +public: + static Logger &Instance(); + + static bool init(bool isServiceLogger); + static void deInit(); + + static bool setServiceLogsEnabled(bool enabled); + + static bool openLogsFolder(bool isServiceLogger); + + static void clearLogs(bool isServiceLogger); + static void clearServiceLogs(); + static void cleanUp(); + + static QString userLogsFilePath(); + static QString serviceLogsFilePath(); + static QString systemLogDir(); + + static QString getLogFile(); + static QString getServiceLogFile(); + + // compat with Mozilla logger + Logger(const QString &className) + { + m_className = className; + } + const QString &className() const + { + return m_className; + } + + class Log + { + public: + Log(Logger *logger, LogLevel level); + ~Log(); + + Log &operator<<(uint64_t t); + Log &operator<<(const char *t); + Log &operator<<(const QString &t); + Log &operator<<(const QStringList &t); + Log &operator<<(const QByteArray &t); + Log &operator<<(const QJsonObject &t); + Log &operator<<(QTextStreamFunction t); + Log &operator<<(const void *t); + + // Q_ENUM + template typename std::enable_if::Value, Log &>::type operator<<(T t) + { + const QMetaObject *meta = qt_getEnumMetaObject(t); + const char *name = qt_getEnumName(t); + addMetaEnum(typename QFlags::Int(t), meta, name); + return *this; + } + + private: + void addMetaEnum(quint64 value, const QMetaObject *meta, const char *name); + + Logger *m_logger; + LogLevel m_logLevel; + + struct Data + { + Data() : m_ts(&m_buffer, QIODevice::WriteOnly) + { + } + + QString m_buffer; + QTextStream m_ts; + }; + + Data *m_data; + }; + + Log error(); + Log warning(); + Log info(); + Log debug(); + QString sensitive(const QString &input); + +private: + Logger() {}; + Logger(Logger const &) = delete; + Logger &operator=(Logger const &) = delete; + + static QString userLogsDir(); + + static QFile m_file; + static QTextStream m_textStream; + static QString m_logFileName; + static QString m_serviceLogFileName; + + friend void debugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + // compat with Mozilla logger + QString m_className; +}; + +#endif // LOGGER_H diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 32fc5f30..cee2c398 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -1,32 +1,32 @@ #include "ipcserver.h" -#include #include -#include #include +#include +#include -#include "router.h" #include "logger.h" +#include "router.h" #include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN -#include "tapcontroller_win.h" -#include "../client/platforms/windows/daemon/windowsfirewall.h" -#include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsfirewall.h" + #include "tapcontroller_win.h" #endif #ifdef Q_OS_LINUX -#include "../client/platforms/linux/daemon/linuxfirewall.h" + #include "../client/platforms/linux/daemon/linuxfirewall.h" #endif #ifdef Q_OS_MACOS -#include "../client/platforms/macos/daemon/macosfirewall.h" + #include "../client/platforms/macos/daemon/macosfirewall.h" #endif -IpcServer::IpcServer(QObject *parent): - IpcInterfaceSource(parent) +IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent) -{} +{ +} int IpcServer::createPrivilegedProcess() { @@ -58,23 +58,10 @@ int IpcServer::createPrivilegedProcess() } }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, [pd](QRemoteObjectNode::ErrorCode errorCode) { - qDebug() << "QRemoteObjectHost::error" << errorCode; - }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, + [pd](QRemoteObjectNode::ErrorCode errorCode) { qDebug() << "QRemoteObjectHost::error" << errorCode; }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { - qDebug() << "QRemoteObjectHost::destroyed"; - }); - -// connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, QProcess::ExitStatus exitStatus){ -// qDebug() << "IpcServerProcess finished" << exitCode << exitStatus; -//// if (m_processes.contains(pid)) { -//// m_processes[pid].ipcProcess.reset(); -//// m_processes[pid].serverNode.reset(); -//// m_processes[pid].localServer.reset(); -//// m_processes.remove(pid); -//// } -// }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); m_processes.insert(m_localpid, pd); @@ -105,7 +92,7 @@ bool IpcServer::routeDeleteList(const QString &gw, const QStringList &ips) qDebug() << "IpcServer::routeDeleteList"; #endif - return Router::routeDeleteList(gw ,ips); + return Router::routeDeleteList(gw, ips); } void IpcServer::flushDns() @@ -158,12 +145,13 @@ void IpcServer::cleanUp() qDebug() << "IpcServer::cleanUp"; #endif - Logger::deinit(); + Logger::deInit(); Logger::cleanUp(); } -void IpcServer::clearLogs() { - Logger::clearLogs(); +void IpcServer::clearLogs() +{ + Logger::clearLogs(true); } bool IpcServer::createTun(const QString &dev, const QString &subnet) @@ -176,7 +164,7 @@ bool IpcServer::deleteTun(const QString &dev) return Router::deleteTun(dev); } -bool IpcServer::updateResolvers(const QString& ifname, const QList& resolvers) +bool IpcServer::updateResolvers(const QString &ifname, const QList &resolvers) { return Router::updateResolvers(ifname, resolvers); } @@ -197,14 +185,12 @@ void IpcServer::setLogsEnabled(bool enabled) #endif if (enabled) { - Logger::init(); - } - else { - Logger::deinit(); + Logger::init(true); + } else { + Logger::deInit(); } } - bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { #ifdef Q_OS_WIN @@ -220,13 +206,11 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd QStringList allownets; QStringList blocknets; - if (splitTunnelType == 0) - { + if (splitTunnelType == 0) { blockAll = true; allowNets = true; allownets.append(configStr.value(amnezia::config_key::hostName).toString()); - } else if (splitTunnelType == 1) - { + } else if (splitTunnelType == 1) { blockNets = true; for (auto v : splitTunnelSites) { blocknets.append(v.toString()); @@ -268,18 +252,17 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd // double-check + ensure our firewall is installed and enabled. This is necessary as // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) - if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + if (!MacOSFirewall::isInstalled()) + MacOSFirewall::install(); MacOSFirewall::ensureRootAnchorPriority(); MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), blockAll); MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), allowNets); - MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, - QStringLiteral("allownets"), allownets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, QStringLiteral("allownets"), allownets); MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), blockNets); - MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, - QStringLiteral("blocknets"), blocknets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, QStringLiteral("blocknets"), blocknets); MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); @@ -330,10 +313,8 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) // Use APP split tunnel if (splitTunnelType == 0 || splitTunnelType == 2) { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("0.0.0.0"), 0)); - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("::"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("0.0.0.0"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("::"), 0)); } if (splitTunnelType == 1) { @@ -341,10 +322,9 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) QString ipRange = v.toString(); if (ipRange.split('/').size() > 1) { config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); + IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); } else { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange), 32)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress(ipRange), 32)); } } } @@ -357,7 +337,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) } } - for (const QJsonValue& i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { + for (const QJsonValue &i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { if (!i.isString()) { break; } diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 234dfafe..c41e57f5 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -19,7 +19,7 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h ${CMAKE_CURRENT_LIST_DIR}/localserver.h - ${CMAKE_CURRENT_LIST_DIR}/logger.h + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h ${CMAKE_CURRENT_LIST_DIR}/router.h ${CMAKE_CURRENT_LIST_DIR}/systemservice.h ${CMAKE_CURRENT_BINARY_DIR}/version.h @@ -31,7 +31,7 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp ${CMAKE_CURRENT_LIST_DIR}/localserver.cpp - ${CMAKE_CURRENT_LIST_DIR}/logger.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp ${CMAKE_CURRENT_LIST_DIR}/router.cpp ${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp @@ -238,6 +238,7 @@ include_directories( ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/../../client ${CMAKE_CURRENT_LIST_DIR}/../../ipc + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger ${CMAKE_CURRENT_BINARY_DIR} ) diff --git a/service/server/logger.cpp b/service/server/logger.cpp deleted file mode 100644 index ab658796..00000000 --- a/service/server/logger.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "logger.h" - -#include -#include -#include -#include - -#include - -#include "version.h" -#include "utilities.h" - -QFile Logger::m_file; -QTextStream Logger::m_textStream; -QString Logger::m_logFileName = QString("%1.log").arg(SERVICE_NAME); - -void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) -{ - if (msg.simplified().isEmpty()) { - return; - } - - Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; - - std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; -} - -bool Logger::init() -{ - if (m_file.isOpen()) return true; - - QString path = Utils::systemLogPath(); - QDir appDir(path); - if (!appDir.mkpath(path)) { - return false; - } - - qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); - - m_file.setFileName(appDir.filePath(m_logFileName)); - if (!m_file.open(QIODevice::Append)) { - qWarning() << "Cannot open log file:" << m_logFileName; - return false; - } - m_file.setTextModeEnabled(true); - m_textStream.setDevice(&m_file); - qInstallMessageHandler(debugMessageHandler); - - return true; -} - -void Logger::deinit() -{ - m_file.close(); - m_textStream.setDevice(nullptr); - qInstallMessageHandler(nullptr); -} - -QString Logger::serviceLogFileNamePath() -{ - return m_file.fileName(); -} - -void Logger::clearLogs() -{ - bool isLogActive = m_file.isOpen(); - m_file.close(); - - - QString path = Utils::systemLogPath(); - QDir appDir(path); - QFile file; - file.setFileName(appDir.filePath(m_logFileName)); - - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.resize(0); - file.close(); - - if (isLogActive) { - init(); - } -} - -void Logger::cleanUp() -{ - clearLogs(); - deinit(); - - QString path = Utils::systemLogPath(); - QDir appDir(path); - - { - QFile file; - file.setFileName(appDir.filePath(m_logFileName)); - file.remove(); - } - { - QFile file; - file.setFileName(appDir.filePath("openvpn.log")); - file.remove(); - } - -#ifdef Q_OS_WINDOWS - QDir dir(Utils::systemLogPath()); - dir.removeRecursively(); -#endif -} - - -Logger::Log::Log(Logger* logger, LogLevel logLevel) - : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) {} - -Logger::Log::~Log() { - qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); - delete m_data; -} - -Logger::Log Logger::error() { return Log(this, LogLevel::Error); } -Logger::Log Logger::warning() { return Log(this, LogLevel::Warning); } -Logger::Log Logger::info() { return Log(this, LogLevel::Info); } -Logger::Log Logger::debug() { return Log(this, LogLevel::Debug); } -QString Logger::sensitive(const QString& input) { -#ifdef Q_DEBUG - return input; -#else - Q_UNUSED(input); - return QString(8, 'X'); -#endif -} - - -#define CREATE_LOG_OP_REF(x) \ -Logger::Log& Logger::Log::operator<<(x t) { \ - m_data->m_ts << t << ' '; \ - return *this; \ -} - -CREATE_LOG_OP_REF(uint64_t); -CREATE_LOG_OP_REF(const char*); -CREATE_LOG_OP_REF(const QString&); -CREATE_LOG_OP_REF(const QByteArray&); -CREATE_LOG_OP_REF(const void*); - -#undef CREATE_LOG_OP_REF - -Logger::Log& Logger::Log::operator<<(const QStringList& t) { - m_data->m_ts << '[' << t.join(",") << ']' << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(const QJsonObject& t) { - m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { - m_data->m_ts << t; - return *this; -} - -void Logger::Log::addMetaEnum(quint64 value, const QMetaObject* meta, - const char* name) { - QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); - - QString out; - QTextStream ts(&out); - - if (const char* scope = me.scope()) { - ts << scope << "::"; - } - - const char* key = me.valueToKey(static_cast(value)); - const bool scoped = me.isScoped(); - if (scoped || !key) { - ts << me.enumName() << (!key ? "(" : "::"); - } - - if (key) { - ts << key; - } else { - ts << value << ")"; - } - - m_data->m_ts << out; -} diff --git a/service/server/logger.h b/service/server/logger.h deleted file mode 100644 index bb920931..00000000 --- a/service/server/logger.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include - -#include "mozilla/shared/loglevel.h" - -class Logger -{ -public: - static bool init(); - static void deinit(); - - static QString serviceLogFileNamePath(); - - static void clearLogs(); - static void cleanUp(); - - // compat with Mozilla logger - Logger(const QString &className) { m_className = className; } - const QString& className() const { return m_className; } - - class Log { - public: - Log(Logger* logger, LogLevel level); - ~Log(); - - Log& operator<<(uint64_t t); - Log& operator<<(const char* t); - Log& operator<<(const QString& t); - Log& operator<<(const QStringList& t); - Log& operator<<(const QByteArray& t); - Log& operator<<(const QJsonObject& t); - Log& operator<<(QTextStreamFunction t); - Log& operator<<(const void* t); - - // Q_ENUM - template - typename std::enable_if::Value, Log&>::type - operator<<(T t) { - const QMetaObject* meta = qt_getEnumMetaObject(t); - const char* name = qt_getEnumName(t); - addMetaEnum(typename QFlags::Int(t), meta, name); - return *this; - } - - private: - void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name); - - Logger* m_logger; - LogLevel m_logLevel; - - struct Data { - Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {} - - QString m_buffer; - QTextStream m_ts; - }; - - Data* m_data; - }; - - Log error(); - Log warning(); - Log info(); - Log debug(); - QString sensitive(const QString& input); - -private: - friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - static QFile m_file; - static QString m_logFileName; - static QTextStream m_textStream; - - // compat with Mozilla logger - QString m_className; -}; - -#endif // LOGGER_H diff --git a/service/server/main.cpp b/service/server/main.cpp index 144ddf60..cee33d72 100644 --- a/service/server/main.cpp +++ b/service/server/main.cpp @@ -44,7 +44,7 @@ int runApplication(int argc, char** argv) int main(int argc, char **argv) { - Utils::initializePath(Utils::systemLogPath()); + Utils::initializePath(Logger::systemLogDir()); if (argc >= 2) { qInfo() << "Started as console application"; From 46058f614e9fd8786967de4d4ae2e717cec4fee3 Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 9 Sep 2024 22:08:06 +0300 Subject: [PATCH 6/7] Add connection checking for WG/AWG via logs (#1056) --- .../amnezia/vpn/protocol/openvpn/OpenVpn.kt | 2 +- .../protocolApi/src/main/kotlin/Protocol.kt | 2 +- .../src/org/amnezia/vpn/AmneziaVpnService.kt | 7 +++- .../vpn/protocol/wireguard/Wireguard.kt | 39 ++++++++++++++++++- client/android/xray/src/main/kotlin/Xray.kt | 2 +- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index abe46245..dd096c60 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -58,7 +58,7 @@ open class OpenVpn : Protocol() { scope = CoroutineScope(Dispatchers.IO) } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val configBuilder = OpenVpnConfig.Builder() openVpnClient = OpenVpnClient( diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index a475a2fc..24cbc595 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -42,7 +42,7 @@ abstract class Protocol { protected abstract fun internalInit() - abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) + abstract suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) abstract fun stopVpn() diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index b30f1503..54330861 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.drop @@ -111,6 +112,10 @@ open class AmneziaVpnService : VpnService() { get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME } private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> + connectionJob?.cancel() + connectionJob = null + disconnectionJob?.cancel() + disconnectionJob = null protocolState.value = DISCONNECTED when (e) { is IllegalArgumentException, @@ -531,7 +536,7 @@ open class AmneziaVpnService : VpnService() { protocolState.value = DISCONNECTING disconnectionJob = connectionScope.launch { - connectionJob?.join() + connectionJob?.cancelAndJoin() connectionJob = null vpnProto?.protocol?.stopVpn() diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 690510eb..b5804a6d 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,7 +1,12 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder +import java.io.IOException +import java.util.Locale import java.util.TreeMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED @@ -79,12 +84,44 @@ open class Wireguard : Protocol() { if (!isInitialized) loadSharedLibrary(context, "wg-go") } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) + val startTime = System.currentTimeMillis() start(wireguardConfig, vpnBuilder, protect) + waitForConnection(startTime) state.value = CONNECTED } + private suspend fun waitForConnection(startTime: Long) { + Log.d(TAG, "Waiting for connection") + withContext(Dispatchers.IO) { + val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0) + try { + delay(1000) + var log = getLogcat(time) + Log.d(TAG, "First waiting log: $log") + // check that there is a connection log, + // to avoid infinite connection + if (!log.contains("Attaching to interface")) { + Log.w(TAG, "Logs do not contain a connection log") + return@withContext + } + while (!log.contains("Received handshake response")) { + delay(1000) + log = getLogcat(time) + } + } catch (e: IOException) { + Log.e(TAG, "Failed to get logcat: $e") + } + } + } + + private fun getLogcat(time: String): String = + ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time) + .redirectErrorStream(true) + .start() + .inputStream.reader().readText() + protected open fun parseConfig(config: JSONObject): WireguardConfig { val configDataJson = config.getJSONObject("wireguard_config_data") val configData = parseConfigData(configDataJson.getString("config")) diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index 3e5f9f7c..94afec2a 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -109,7 +109,7 @@ class Xray : Protocol() { } } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { if (isRunning) { Log.w(TAG, "XRay already running") return From 581773ce039c476d0d086e1fb0b01c7705409c4e Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 9 Sep 2024 22:10:48 +0300 Subject: [PATCH 7/7] Bump version to 4.8.0.0 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41e05838..4f22016a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.7.0.0 +project(${PROJECT} VERSION 4.8.0.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 57) +set(APP_ANDROID_VERSION_CODE 58) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux")