I'm writing an utility on the Mac and need to auto-determine proxy information. I've managed to get the proxy host and port (from an automatic proxy configuration file), however how do I obtain the username from the keychain from this?
I know you can use SecKeychainAddInternetPassword to get the proxy password, but I don't know the username either. Is there a way to get the username AND the password?
I found it a little complicated. First, you have to ask the system configuration whether the proxy is on or off. Then, you have to ask the keychain if it has any account on the returned hostname for a given proxy (HTTP, HTTPS, etc.). Then, if the keychain says yes, you can get the username from the result and ask the keychain for the matching password. At this point, the user may see an alert asking to allow your app to access the password.
Here's some sample code (Mac OS X 10.6+, ARC).
ProxyDetector.h:
#import <Foundation/Foundation.h>
@interface ProxyDetector : NSObject
-(ProxyDetector *)init;
-(void)detectHttpProxyReturningHostname:(NSString **)hostName port:(int *)port username:(NSString **)username password:(NSString **)password;
-(void)detectHttpsProxyReturningHostname:(NSString **)hostName port:(int *)port username:(NSString **)username password:(NSString **)password;
@end
ProxyDetector.m:
#import "ProxyDetector.h"
#import <SystemConfiguration/SCDynamicStoreCopySpecific.h>
#import <SystemConfiguration/SCSchemaDefinitions.h>
#import <Security/Security.h>
@implementation ProxyDetector
-(ProxyDetector *)init;
{
if ((self = [super init])) {
// init
}
return self;
}
void detectProxyWithParams(CFStringRef proxyEnableKey, CFStringRef proxyHostNameKey, CFStringRef proxyPortKey, CFTypeRef proxyProtocol, UInt32 proxyProtocolCode, NSString **hostNamePtr, int *portPtr, NSString **usernamePtr, NSString **passwordPtr)
{
// get general proxy info
CFDictionaryRef proxyInfoCPtr = SCDynamicStoreCopyProxies(NULL);
NSDictionary *proxyInfo = (__bridge NSDictionary *) proxyInfoCPtr;
NSNumber *proxyEnabled = proxyInfo[(__bridge NSString *)proxyEnableKey];
// prefill null values for data we may not set later
*usernamePtr = nil;
*passwordPtr = nil;
// is it enabled?
if (![proxyEnabled intValue]) {
*hostNamePtr = nil;
*portPtr = 0;
return;
}
// we can get hostname and port number from this info, but not username and password
*hostNamePtr = proxyInfo[(__bridge NSString *)proxyHostNameKey];
NSNumber *portNumber = proxyInfo[(__bridge NSString *)proxyPortKey];
*portPtr = [portNumber intValue];
// check in the keychain for username and password
CFArrayRef result = NULL;
OSStatus status = SecItemCopyMatching(
(__bridge CFDictionaryRef) [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)kSecClassInternetPassword, kSecClass,
kSecMatchLimitAll, kSecMatchLimit,
kCFBooleanTrue, kSecReturnAttributes,
proxyProtocol, kSecAttrProtocol,
nil],
(CFTypeRef *) &result
);
if (status != noErr) {
if (status != errSecItemNotFound) {
// unexpected error (else, just no password)
NSString *errorStr = (__bridge NSString *)SecCopyErrorMessageString(status, NULL);
NSLog(@"Error while trying to find proxy username and password for hostname %@; assuming no password: %@", *hostNamePtr, errorStr);
}
return;
}
// check what the keychain got us as results
CFIndex resultCount = CFArrayGetCount(result);
for (CFIndex resultIndex = 0; resultIndex < resultCount; resultIndex++) {
NSDictionary *attrs = (NSDictionary *) CFArrayGetValueAtIndex(result, resultIndex);
// check if the found host matches the host we got earlier
NSString *host = [attrs objectForKey:(id)kSecAttrServer];
if (![host isEqualToString:*hostNamePtr])
continue;
const char *hostCStr = [host UTF8String];
NSString *username = [attrs objectForKey:(id)kSecAttrAccount];
const char *usernameCStr = [username UTF8String];
// we know the username now, so ask keychain for the password
UInt32 passwordLength;
void *passwordData;
// this may trigger UI interaction to allow the password to be accessed by this app
status = SecKeychainFindInternetPassword(NULL, // default user keychains
(UInt32)strlen(hostCStr), hostCStr,
0, NULL, // no security domain
(UInt32)strlen(usernameCStr), usernameCStr,
0, NULL, // no path
0, // ignore port
proxyProtocolCode,
kSecAuthenticationTypeAny,
&passwordLength, &passwordData, NULL);
if (status != noErr) {
// error getting or accessing this password
NSString *errorStr = (__bridge NSString *)SecCopyErrorMessageString(status, NULL);
NSLog(@"Error while trying to find proxy username and password for hostname %@; assuming no password: %@", *hostNamePtr, errorStr);
} else {
// we got everything we needed
*usernamePtr = username;
*passwordPtr = [NSString stringWithUTF8String:passwordData];
break; // only one valid item in the results here anyway
}
}
CFRelease(result);
}
-(void)detectHttpProxyReturningHostname:(NSString **)hostName port:(int *)port username:(NSString **)username password:(NSString **)password;
{
detectProxyWithParams(kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPProxy, kSCPropNetProxiesHTTPPort, kSecAttrProtocolHTTPProxy, kSecProtocolTypeHTTPProxy, hostName, port, username, password);
}
-(void)detectHttpsProxyReturningHostname:(NSString **)hostName port:(int *)port username:(NSString **)username password:(NSString **)password;
{
detectProxyWithParams(kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSProxy, kSCPropNetProxiesHTTPSPort, kSecAttrProtocolHTTPSProxy, kSecProtocolTypeHTTPSProxy, hostName, port, username, password);
}
@end
Usage example:
NSString *hostName;
int port;
NSString *username;
NSString *password;
ProxyDetector *proxyDetector = [[ProxyDetector alloc] init];
[proxyDetector detectHttpProxyReturningHostname:&hostName port:&port username:&username password:&password];
if (hostName) {
if (username) {
NSLog(@"HTTP proxy with authentication: http://%@:%@@%@:%d", username, password, hostName, port);
} else {
NSLog(@"HTTP proxy without authentication: http://%@:%d", hostName, port);
}
} else {
NSLog(@"No HTTP proxy");
}
[proxyDetector detectHttpsProxyReturningHostname:&hostName port:&port username:&username password:&password];
if (hostName) {
if (username) {
NSLog(@"HTTPS proxy with authentication: http://%@:%@@%@:%d", username, password, hostName, port);
} else {
NSLog(@"HTTPS proxy without authentication: http://%@:%d", hostName, port);
}
} else {
NSLog(@"No HTTPS proxy");
}
Improvements are welcome!