SYNERGY-1032 - Use native notifications for secure input on MacOS (#7041)

* SYNERGY-1017 - Try usnig NSUserNotification

* SYNERGY-1032 - Use UNUserNotificationCenter instead of deprecated notifications

* SYNERGY-1032 - Update app delegate and add log after notification access

* SYNERGY-1032 - Try using deprecated notification

* SYNERGY-1032 - Add notifications to the platform dependant code

* SYNERGY-1032 - Check for development builds before asking permission for notifications

* SYNERGY-1032 - Remove old notification and secure input detection

* SYNERGY-1032 - Add permission request to the main app

* SYNERGY-1032 - Only show notifications on server

* SYNERGY-1032 - Request and show notification from main app

* SYNERGY-1032 - Remove logging from OSX helpers

* SYNERGY-1032 - Add UN notification for testing

* SYNERGY-1032 - Show notification once

* SYNERGY-1032 - Try replicating old flow

* SYNERGY-1032 - Reuse notification center

* SYNERGY-1032 - Send notification after granting permission

* SYNERGY-1032 - Update app delegate

* SYNERGY-1032 - Remove deprecated property

* SYNERGY-1032 - Add output for debugging

* SYNERGY-1032 - Try updating delegate

* SYNERGY-1032 - Print current bundle name

* SYNERGY-1032 - Add AppDelegate to the server

* SYNERGY-1032 - Move notification creation to synergy app

* SYNERGY-1032 - Update notification text

* SYNERGY-1032 - Fix QString conversion for notification

* SYNERGY-1032 - Move notification creation outside the handler

* SYNERGY-1032 - Remove debug notification, show notification with no delay

* SYNERGY-1032 - Show notification inside the completion handler

* SYNERGY-1032 - Remove timed notification trigger

* SYNERGY-1032 - Request permissions on synergy start

* SYNERGY-1032 - Remove unused includes

* SYNERGY-1032 - Update changelog

* SYNERGY-1032 - Add empty notification implementations for Windows and Linux

* SYNERGY-1032 - Removed temporary debug messages

* SYNERGY-1032 - Resolve code smell
This commit is contained in:
Igor Sikachyna 2021-06-29 13:20:56 +03:00 committed by GitHub
parent c58b69dfad
commit 5843230169
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 277 additions and 107 deletions

View file

@ -167,6 +167,7 @@ if (UNIX)
find_library (lib_ApplicationServices ApplicationServices)
find_library (lib_Foundation Foundation)
find_library (lib_Carbon Carbon)
find_library (lib_UserNotifications UserNotifications)
list (APPEND libs
${lib_ScreenSaver}
@ -174,6 +175,7 @@ if (UNIX)
${lib_ApplicationServices}
${lib_Foundation}
${lib_Carbon}
${lib_UserNotifications}
)
else() # not-apple

View file

@ -15,7 +15,7 @@ Enhancements:
- #7026 Update the way we package OpenSSL for Windows build
- #7035 Add Fedora 34 build
- #7037 Add Ubuntu 21.04 build
- #7030 Add user nofication for secure input on Mac
- #7030 | #7041 Add user nofication for secure input on Mac
===========
v1.14.0-stable

17
src/gui/src/AppDelegate.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef APPDELEGATE_H
#define APPDELEGATE_H
#ifdef __cplusplus
extern "C" {
#endif
#import <Cocoa/Cocoa.h>
#import <UserNotifications/UNUserNotificationCenter.h>
@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate, UNUserNotificationCenterDelegate>
@end
#ifdef __cplusplus
}
#endif
#endif // APPDELEGATE_H

View file

@ -0,0 +1,33 @@
#import "AppDelegate.h"
@interface AppDelegate ()
@property (strong) IBOutlet NSWindow *window;
@end
@implementation AppDelegate
{
}
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
[[UNUserNotificationCenter currentNotificationCenter] setDelegate:self];
}
-(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification{
return YES;
}
-(void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
UNNotificationPresentationOptions presentationOptions =
UNNotificationPresentationOptionSound
| UNNotificationPresentationOptionAlert
| UNNotificationPresentationOptionBadge;
completionHandler(presentationOptions);
}
@end

View file

@ -328,42 +328,6 @@ void MainWindow::saveSettings()
GUI::Config::ConfigWriter::make()->globalSave();
}
void MainWindow::checkSystemInterruptions()
{
if(synergyType() == synergyServer)
{
#if defined(Q_OS_MAC)
if(!isOSXSecureInputEnabled())
{
m_isSecureInputNotificationShown = false;
}
else if(!m_isSecureInputNotificationShown)
{
// avoid showing multiple duplicate messages
m_isSecureInputNotificationShown = true;
QMessageBox message(this);
message.addButton(QObject::tr("Accept"), QMessageBox::AcceptRole);
std::string messageText =
"Secure input was enabled in your system by another application. " \
"Synergy will not be able to send keyboard strokes while the secure input is enabled\n\n";
int secureInputProcessPID = getOSXSecureInputEventPID();
std::string infringingProcessName = getOSXProcessName(secureInputProcessPID);
// IO registry may not contain the secure input process PID
// in this case don't add an option to quit the infringing app
if(secureInputProcessPID == 0) infringingProcessName = "unknown";
else message.addButton(QString("Quit %1").arg(infringingProcessName.c_str()), QMessageBox::ApplyRole);
messageText += "Infringing process is " + infringingProcessName;
message.setText(QObject::tr(messageText.c_str()));
// if user decides to stop the app send the SIGTERM signal
if (message.exec() == QMessageBox::Accepted && secureInputProcessPID) kill(secureInputProcessPID, SIGTERM);
}
#endif
}
}
void MainWindow::zeroConfToggled() {
#if !defined(SYNERGY_ENTERPRISE) && defined(SYNERGY_AUTOCONFIG)
updateZeroconfService();
@ -498,6 +462,12 @@ void MainWindow::updateFromLogLine(const QString &line)
checkFingerprint(line);
checkSecureSocket(line);
// subprocess (synergys, synergyc) is not allowed to show notifications
// process the log from it and show notificatino from synergy instead
#ifdef Q_OS_MAC
checkOSXNotification(line);
#endif
#ifndef SYNERGY_ENTERPRISE
checkLicense(line);
#endif
@ -609,6 +579,27 @@ void MainWindow::checkSecureSocket(const QString& line)
m_SecureSocketVersion = line.mid(index + strlen(tlsCheckString)); // Compliant: we made sure that tlsCheckString variable ended with null(static const char* declaration)
}
}
#ifdef Q_OS_MAC
void MainWindow::checkOSXNotification(const QString& line)
{
static const QString OSXNotificationSubstring = "OSX Notification: ";
if (line.contains(OSXNotificationSubstring) && line.contains('|')) {
int delimterPosition = line.indexOf('|');
int notificationStartPosition = line.indexOf(OSXNotificationSubstring);
QString title =
line.mid(notificationStartPosition + OSXNotificationSubstring.length(),
delimterPosition - notificationStartPosition - OSXNotificationSubstring.length());
QString body =
line.mid(delimterPosition + 1,
line.length() - delimterPosition);
if (!showOSXNotification(title, body)) {
appendLogInfo("OSX notification was not shown");
}
}
}
#endif
QString MainWindow::getTimeStamp()
{
QDateTime current = QDateTime::currentDateTime();
@ -646,6 +637,10 @@ void MainWindow::startSynergy()
{
saveSettings();
#ifdef Q_OS_MAC
requestOSXNotificationPermission();
#endif
#ifndef SYNERGY_ENTERPRISE
SerialKey serialKey = m_LicenseManager->serialKey();
if (!serialKey.isValid()) {
@ -743,7 +738,6 @@ void MainWindow::startSynergy()
connect(synergyProcess(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(synergyFinished(int, QProcess::ExitStatus)));
connect(synergyProcess(), SIGNAL(readyReadStandardOutput()), this, SLOT(logOutput()));
connect(synergyProcess(), SIGNAL(readyReadStandardError()), this, SLOT(logError()));
connect(&m_systemInterruptionCheckTimer, SIGNAL(timeout()), this, SLOT(checkSystemInterruptions()));
}
qDebug() << args;
@ -1061,7 +1055,6 @@ void MainWindow::setSynergyState(qSynergyState state)
connect (m_pButtonToggleStart, SIGNAL(clicked()), m_pActionStopSynergy, SLOT(trigger()));
m_pButtonToggleStart->setText(tr("&Stop"));
m_pButtonApply->setEnabled(true);
m_systemInterruptionCheckTimer.start(5000); // check every 5 seconds
}
else if (state == synergyDisconnected)
{
@ -1069,7 +1062,6 @@ void MainWindow::setSynergyState(qSynergyState state)
connect (m_pButtonToggleStart, SIGNAL(clicked()), m_pActionStartSynergy, SLOT(trigger()));
m_pButtonToggleStart->setText(tr("&Start"));
m_pButtonApply->setEnabled(false);
m_systemInterruptionCheckTimer.stop();
}
bool running = false;

View file

@ -164,7 +164,6 @@ public slots:
void logError();
void updateFound(const QString& version);
void saveSettings();
void checkSystemInterruptions();
/// @brief Receives the signal that the auto config option has changed
void zeroConfToggled();
@ -207,6 +206,9 @@ public slots:
void checkConnected(const QString& line);
void checkFingerprint(const QString& line);
void checkSecureSocket(const QString& line);
#ifdef Q_OS_MAC
void checkOSXNotification(const QString& line);
#endif
#ifndef SYNERGY_ENTERPRISE
void checkLicense(const QString& line);
#endif
@ -247,11 +249,6 @@ public slots:
QString m_SecureSocketVersion; // brief Contains the version of the Secure Socket currently active
ServerConnection m_serverConnection;
ClientConnection m_clientConnection;
QTimer m_systemInterruptionCheckTimer; // Timer used for sceduling a task for detecting the system
// operations preventing the Synergy from working properly
#if defined(Q_OS_MAC)
bool m_isSecureInputNotificationShown = false;
#endif
void updateAutoConfigWidgets();

View file

@ -19,7 +19,7 @@
#define OSXHELPERS__H
#include <string>
#include <QString>
enum class IconsTheme {
ICONS_DARK,
@ -27,9 +27,9 @@ enum class IconsTheme {
ICONS_TEMPLATE
};
bool isOSXSecureInputEnabled();
int getOSXSecureInputEventPID();
std::string getOSXProcessName(int pid);
void requestOSXNotificationPermission();
bool isOSXDevelopmentBuild();
bool showOSXNotification(const QString& title, const QString& body);
bool isOSXInterfaceStyleDark();
IconsTheme getOSXIconsTheme();

View file

@ -20,76 +20,83 @@
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import <Cocoa/Cocoa.h>
#import <base/Log.h>
#import <UserNotifications/UNNotification.h>
#import <UserNotifications/UNUserNotificationCenter.h>
#import <UserNotifications/UNNotificationContent.h>
#import <UserNotifications/UNNotificationTrigger.h>
#import <Carbon/Carbon.h>
#import <mach/mach_port.h>
#import <mach/mach_interface.h>
#import <mach/mach_init.h>
#import <IOKit/IOMessage.h>
#import <IOKit/IOKitLib.h>
#import <libproc.h>
#import <QtGlobal>
void requestOSXNotificationPermission()
{
if (@available(macOS 10.14, *))
{
if (isOSXDevelopmentBuild())
{
qWarning("Not requesting notification permission in dev build");
return;
}
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if(error != nil)
{
qWarning("Notification permission request error: %s", [[NSString stringWithFormat:@"%@", error] UTF8String]);
}
}];
}
}
bool
isOSXSecureInputEnabled()
isOSXDevelopmentBuild()
{
return IsSecureEventInputEnabled();
std::string bundleURL = [[[NSBundle mainBundle] bundleURL].absoluteString UTF8String];
return (bundleURL.find("Applications/Synergy.app") == std::string::npos);
}
int
getOSXSecureInputEventPID()
bool
showOSXNotification(const QString& title, const QString& body)
{
io_service_t service = MACH_PORT_NULL, service_root = MACH_PORT_NULL;
mach_port_t masterPort;
if (@available(macOS 10.14, *))
{
// accessing notification center on unsigned build causes an immidiate
// application shutodown (in this case synergys) and cannot be caught
// to avoid issues with it need to first check if this is a dev build
if (isOSXDevelopmentBuild())
{
qWarning("Not showing notification in dev build");
return false;
}
kern_return_t kr = IOMasterPort( MACH_PORT_NULL, &masterPort );
if(kr != KERN_SUCCESS) return 0;
requestOSXNotificationPermission();
// IO registry refuses to tap into the root level directly
// as a workaround access the parent of the top user level
service = IORegistryEntryFromPath( masterPort, kIOServicePlane ":/" );
IORegistryEntryGetParentEntry(service, kIOServicePlane, &service_root);
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
std::unique_ptr<std::remove_pointer<CFTypeRef>::type, decltype(&CFRelease)> consoleUsers(
IORegistryEntrySearchCFProperty(service_root, kIOServicePlane, CFSTR("IOConsoleUsers"), NULL, kIORegistryIterateParents | kIORegistryIterateRecursively),
CFRelease
);
if(!consoleUsers) return 0;
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = title.toNSString();
content.body = body.toNSString();
CFTypeID type = CFGetTypeID(consoleUsers.get());
if(type != CFArrayGetTypeID()) return 0;
// Create the request object.
UNNotificationRequest* request = [UNNotificationRequest
requestWithIdentifier:@"SecureInput" content:content trigger:nil];
CFTypeRef dict = CFArrayGetValueAtIndex((CFArrayRef)consoleUsers.get(), 0);
if(!dict) return 0;
type = CFGetTypeID(dict);
if(type != CFDictionaryGetTypeID()) return 0;
CFTypeRef secureInputPID = nullptr;
CFDictionaryGetValueIfPresent((CFDictionaryRef)dict, CFSTR("kCGSSessionSecureInputPID"), &secureInputPID);
if(secureInputPID == nullptr) return 0;
type = CFGetTypeID(secureInputPID);
if(type != CFNumberGetTypeID()) return 0;
auto pidRef = (CFNumberRef)secureInputPID;
CFNumberType numberType = CFNumberGetType(pidRef);
if(numberType != kCFNumberSInt32Type) return 0;
int pid;
CFNumberGetValue(pidRef, kCFNumberSInt32Type, &pid);
return pid;
}
std::string
getOSXProcessName(int pid)
{
if(!pid) return "";
char buf[128];
proc_name(pid, buf, sizeof(buf));
return buf;
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
qWarning("Notification display request error: %s", [[NSString stringWithFormat:@"%@", error] UTF8String]);
}
}];
}
else
{
NSUserNotification* notification = [[NSUserNotification alloc] init];
notification.title = title.toNSString();
notification.informativeText = body.toNSString();
notification.soundName = NSUserNotificationDefaultSoundName; //Will play a default sound
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notification];
[notification autorelease];
}
return true;
}
bool

View file

@ -1988,6 +1988,12 @@ MSWindowsScreen::getDropTarget() const
return m_desktopPath;
}
void
MSWindowsScreen::createNotification(const String& title, const String& content) const
{
// TODO: implement notification
}
bool
MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask state, WPARAM wParam) const
{

View file

@ -139,6 +139,7 @@ public:
virtual String& getDraggingFilename();
virtual const String&
getDropTarget() const;
void createNotification(const String& title, const String& content) const override;
protected:
// IPlatformScreen overrides

View file

@ -101,6 +101,7 @@ public:
const String& getDropTarget() const { return m_dropTarget; }
void waitForCarbonLoop() const;
void createNotification(const String& title, const String& content) const override;
protected:
// IPlatformScreen overrides
@ -202,6 +203,8 @@ private:
static char* CFStringRefToUTF8String(CFStringRef aString);
void getDropTargetThread(void*);
void createSecureInputNotification();
private:
struct HotKeyItem {

View file

@ -46,6 +46,7 @@
#include <AvailabilityMacros.h>
#include <IOKit/hidsystem/event_status_driver.h>
#include <AppKit/NSEvent.h>
#include <libproc.h>
// This isn't in any Apple SDK that I know of as of yet.
enum {
@ -58,6 +59,9 @@ enum {
kCarbonLoopWaitTimeout = 10
};
int getSecureInputEventPID();
String getProcessName(int pid);
// TODO: upgrade deprecated function usage in these functions.
void setZeroSuppressionInterval();
void avoidSupression();
@ -881,6 +885,10 @@ OSXScreen::enter()
bool
OSXScreen::leave()
{
if(m_isPrimary && IsSecureEventInputEnabled()) {
createSecureInputNotification();
}
hideCursor();
if (isDraggingStarted()) {
@ -2150,6 +2158,84 @@ OSXScreen::waitForCarbonLoop() const
}
void
OSXScreen::createNotification(const String& title, const String& content) const
{
LOG((CLOG_INFO "OSX Notification: %s|%s", title.c_str(), content.c_str()));
}
void
OSXScreen::createSecureInputNotification()
{
std::string secureInputNotificationBody =
"Secure input was enabled in your system. " \
"Synergy may not be able to send keyboard strokes. ";
int secureInputProcessPID = getSecureInputEventPID();
std::string infringingProcessName = getProcessName(secureInputProcessPID);
if(secureInputProcessPID == 0) infringingProcessName = "unknown";
secureInputNotificationBody += "Infringing process is " + infringingProcessName;
createNotification(
"Keyboard may not work correctly",
secureInputNotificationBody);
}
int
getSecureInputEventPID()
{
io_service_t service = MACH_PORT_NULL, service_root = MACH_PORT_NULL;
mach_port_t masterPort;
kern_return_t kr = IOMasterPort( MACH_PORT_NULL, &masterPort );
if(kr != KERN_SUCCESS) return 0;
// IO registry refuses to tap into the root level directly
// as a workaround access the parent of the top user level
service = IORegistryEntryFromPath( masterPort, kIOServicePlane ":/" );
IORegistryEntryGetParentEntry(service, kIOServicePlane, &service_root);
std::unique_ptr<std::remove_pointer<CFTypeRef>::type, decltype(&CFRelease)> consoleUsers(
IORegistryEntrySearchCFProperty(service_root, kIOServicePlane, CFSTR("IOConsoleUsers"), NULL, kIORegistryIterateParents | kIORegistryIterateRecursively),
CFRelease
);
if(!consoleUsers) return 0;
CFTypeID type = CFGetTypeID(consoleUsers.get());
if(type != CFArrayGetTypeID()) return 0;
CFTypeRef dict = CFArrayGetValueAtIndex((CFArrayRef)consoleUsers.get(), 0);
if(!dict) return 0;
type = CFGetTypeID(dict);
if(type != CFDictionaryGetTypeID()) return 0;
CFTypeRef secureInputPID = nullptr;
CFDictionaryGetValueIfPresent((CFDictionaryRef)dict, CFSTR("kCGSSessionSecureInputPID"), &secureInputPID);
if(secureInputPID == nullptr) return 0;
type = CFGetTypeID(secureInputPID);
if(type != CFNumberGetTypeID()) return 0;
auto pidRef = (CFNumberRef)secureInputPID;
CFNumberType numberType = CFNumberGetType(pidRef);
if(numberType != kCFNumberSInt32Type) return 0;
int pid;
CFNumberGetValue(pidRef, kCFNumberSInt32Type, &pid);
return pid;
}
String
getProcessName(int pid)
{
if(!pid) return "";
char buf[128];
proc_name(pid, buf, sizeof(buf));
return buf;
}
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
void

View file

@ -475,6 +475,12 @@ XWindowsScreen::isPrimary() const
return m_isPrimary;
}
void
XWindowsScreen::createNotification(const String& title, const String& content) const
{
// TODO: implement notification
}
void*
XWindowsScreen::getEventTarget() const
{

View file

@ -84,6 +84,7 @@ public:
virtual void setOptions(const OptionsList& options);
virtual void setSequenceNumber(UInt32);
virtual bool isPrimary() const;
void createNotification(const String& title, const String& content) const override;
protected:
// IPlatformScreen overrides

View file

@ -133,6 +133,12 @@ public:
//! Change dragging status
virtual void setDraggingStarted(bool started) = 0;
//! Send desktop user notification
/*!
Creates a platform dependant user notificaiton using native APIs
*/
virtual void createNotification(const String& title, const String& content) const = 0;
//@}
//! @name accessors
//@{

View file

@ -522,4 +522,10 @@ Screen::leaveSecondary()
m_screen->fakeAllKeysUp();
}
void
Screen::createNotification(const String& title, const String& content) const
{
m_screen->createNotification(title, content);
}
}

View file

@ -233,6 +233,13 @@ public:
void startDraggingFiles(DragFileList& fileList);
void setEnableDragDrop(bool enabled);
//! Send desktop user notification
/*!
Creates a platform dependant user notificaiton using native APIs
*/
virtual void createNotification(const String& title, const String& content) const;
//@}
//! @name accessors
//@{