Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion apps/finicky/assets/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
<string>4.4.0-alpha</string>
<key>CFBundleIconFile</key>
<string>finicky.icns</string>
<key>ASWebAuthenticationSessionWebBrowserSupportCapabilities</key>
<dict>
<key>IsSupported</key>
<true/>
<key>CallbackURLMatchingIsSupported</key>
<true/>
</dict>
<key>LSUIElement</key>
<true/>
<key>CFBundleURLTypes</key>
Expand All @@ -35,6 +42,15 @@
<array>
<string>http</string>
<string>https</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>Finicky URL</string>
<key>CFBundleURLSchemes</key>
<array>
<string>finicky</string>
</array>
</dict>
Expand Down Expand Up @@ -77,4 +93,4 @@
<string>NSUserActivityTypeBrowsingWeb</string>
</array>
</dict>
</plist>
</plist>
3 changes: 2 additions & 1 deletion apps/finicky/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package main

/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa -framework CoreServices
#cgo LDFLAGS: -framework Cocoa -framework CoreServices -framework AuthenticationServices
#include <stdlib.h>
#include "main.h"
*/
Expand Down Expand Up @@ -68,6 +68,7 @@ func main() {
startTime := time.Now()
logger.Setup()
runtime.LockOSThread()
C.InstallAuthenticationSessionHandler()

// Define command line flags
configPathPtr := flag.String("config", "", "Path to custom JS configuration file")
Expand Down
1 change: 1 addition & 0 deletions apps/finicky/src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ extern char* GetCurrentConfigPath();
#endif

void RunApp(bool forceOpenWindow, bool showStatusItem, bool keepRunning);
void InstallAuthenticationSessionHandler(void);

#endif /* MAIN_H */
105 changes: 102 additions & 3 deletions apps/finicky/src/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,97 @@
#include "util/info.h"
#import <Cocoa/Cocoa.h>
#import <ApplicationServices/ApplicationServices.h>
#import <AuthenticationServices/AuthenticationServices.h>
#import <stdlib.h>
#import <unistd.h>

#import "window/window.h" // For ShowWindow()

@interface FinickyAuthenticationSessionHandler : NSObject<ASWebAuthenticationSessionWebBrowserSessionHandling>
@property (nonatomic, strong) NSMutableDictionary<NSUUID *, ASWebAuthenticationSessionRequest *> *requests;
- (BOOL)completeRequestWithCallbackURL:(NSURL *)url;
@end

@implementation FinickyAuthenticationSessionHandler

- (instancetype)init {
self = [super init];
if (self) {
_requests = [[NSMutableDictionary alloc] init];
}
return self;
}

- (void)beginHandlingWebAuthenticationSessionRequest:(ASWebAuthenticationSessionRequest *)request {
if (!request.URL) {
NSError *error = [NSError errorWithDomain:@"se.johnste.finicky.authentication"
code:1
userInfo:@{NSLocalizedDescriptionKey: @"Authentication request did not include a URL"}];
[request cancelWithError:error];
return;
}

self.requests[request.UUID] = request;

// AuthenticationServices does not expose the requesting app here, and
// Finicky cannot transfer the ASWebAuthenticationSessionRequest to the
// browser it chooses. Treat this as a normal URL routing request with a
// stable synthetic opener so rules can target SSO/OAuth flows explicitly.
HandleURL((char *)[[request.URL absoluteString] UTF8String],
"AuthenticationServices",
"com.apple.AuthenticationServices",
"",
NULL,
false);
}

- (void)cancelWebAuthenticationSessionRequest:(ASWebAuthenticationSessionRequest *)request {
[self.requests removeObjectForKey:request.UUID];
}

- (BOOL)completeRequestWithCallbackURL:(NSURL *)url {
if (!url.scheme) {
return NO;
}

// Callback matching is best-effort: Finicky can complete callbacks that
// re-enter Finicky, but it cannot observe navigations after forwarding the
// auth URL to the concrete browser selected by the user's rules.
for (NSUUID *uuid in [self.requests allKeys]) {
ASWebAuthenticationSessionRequest *request = self.requests[uuid];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSString *callbackURLScheme = request.callbackURLScheme;
#pragma clang diagnostic pop
if (callbackURLScheme.length == 0) {
continue;
}
if ([callbackURLScheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[callbackURLScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
continue;
}
if ([url.scheme caseInsensitiveCompare:callbackURLScheme] == NSOrderedSame) {
[request completeWithCallbackURL:url];
[self.requests removeObjectForKey:uuid];
return YES;
}
}

return NO;
}

@end

static FinickyAuthenticationSessionHandler *sharedAuthenticationSessionHandler;

void InstallAuthenticationSessionHandler(void) {
if (!sharedAuthenticationSessionHandler) {
sharedAuthenticationSessionHandler = [[FinickyAuthenticationSessionHandler alloc] init];
}

[ASWebAuthenticationSessionWebBrowserSessionManager sharedManager].sessionHandler = sharedAuthenticationSessionHandler;
}

// Extend BrowseAppDelegate to hold a status item and declare menu action
@interface BrowseAppDelegate ()
@property (nonatomic, strong) NSStatusItem *statusItem;
Expand All @@ -30,16 +116,17 @@ - (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)sho
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
[self terminateOtherInstances];

bool launchedByAuthenticationServices = [ASWebAuthenticationSessionWebBrowserSessionManager sharedManager].wasLaunchedByAuthenticationServices;
bool openWindow = self.forceOpenWindow;
if (!openWindow) {
// Even if we aren't forcing the window to open, we still want to open it if didn't receive a URL
openWindow = !self.receivedURL;
openWindow = !self.receivedURL && !launchedByAuthenticationServices;
}

// Only show menu item if the option is enabled, and we either didn't receive a URL or we are keeping
// the application running. We don't want to show the icon if Finicky is just receiving a url to open
// and is expected to exit after
if (self.showMenuItem && (self.keepRunning || !self.receivedURL)) {
if (self.showMenuItem && (self.keepRunning || (!self.receivedURL && !launchedByAuthenticationServices))) {
[self createStatusItem];
}

Expand Down Expand Up @@ -162,6 +249,8 @@ - (void)applicationWillFinishLaunching:(NSNotification *)aNotification
[appleEventManager setEventHandler:self
andSelector:@selector(handleGetURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass andEventID:kAEGetURL];

InstallAuthenticationSessionHandler();
}

- (bool)application:(NSApplication *)sender openFile:(NSString *)filename {
Expand All @@ -185,13 +274,19 @@ - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
// Get the application that opened the URL, if available
int32_t pid = [[event attributeDescriptorForKeyword:keySenderPIDAttr] int32Value];
NSRunningApplication *application = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
const char *url = [[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String];
NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
const char *url = [urlString UTF8String];
const char *name = NULL;
const char *bundleId = NULL;
const char *path = NULL;

self.receivedURL = true;

NSURL *eventURL = [NSURL URLWithString:urlString];
if ([sharedAuthenticationSessionHandler completeRequestWithCallbackURL:eventURL]) {
return;
}

NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];

// Assume Finicky is in front if we are not keeping running, since there's no good way
Expand Down Expand Up @@ -249,6 +344,10 @@ - (bool)application:(NSApplication *)application continueUserActivity:(NSUserAct
return false;
}

if ([sharedAuthenticationSessionHandler completeRequestWithCallbackURL:url]) {
return true;
}

HandleURL((char*)[[url absoluteString] UTF8String], NULL, NULL, NULL, NULL, false);
return true;
}
Expand Down
Loading