Skip to content

Instantly share code, notes, and snippets.

@jorgenpt
Last active December 27, 2015 00:59
Show Gist options
  • Save jorgenpt/7241694 to your computer and use it in GitHub Desktop.
Save jorgenpt/7241694 to your computer and use it in GitHub Desktop.
Utility class to retrieve HTTPS certificates from a server.
#import <Foundation/Foundation.h>
@protocol HTTPSCertificateRetrieverDelegate <NSObject>
/**
* Called when we succeed (or fail) to retrieve the certificate.
*
* @param certificate nil if we fail, otherwise the data of the certificate.
*/
- (void)certificateRetrieved:(NSData*)certificate;
@end
@interface HTTPSCertificateRetriever : NSObject
/// Server's HTTPS certificate, nil before delegate has received certificateRetrieved.
@property (readonly) NSData *certificate;
@property (weak) id<HTTPSCertificateRetrieverDelegate> delegate;
/// Instantiate a certificate retriever for the given HTTPS URL.
- (instancetype)initWithURL:(NSURL*)url
delegate:(id<HTTPSCertificateRetrieverDelegate>)delegate;
/// Asynchronously start the HTTPS request on the current CFRunLoop.
- (void)startWithTimeout:(NSTimeInterval)timeout
retries:(NSInteger)retries;
/// Stop the certificate attempt.
- (void)stop;
/// YES if the certificate retrieving request has completed.
- (BOOL)hasCompleted;
@end
#import "HTTPSCertificateRetriever.h"
#import "NSTimer+Blocks.h"
@interface HTTPSCertificateRetriever () <NSURLConnectionDelegate>
@property (strong) NSData *certificate;
@property (strong) NSInputStream *readStream;
@property (strong) NSTimer *timeoutTimer;
@property (assign) BOOL hasCompleted;
@property (strong) id request;
- (void)handleStreamEvent:(CFStreamEventType)event;
@end
static void bridgeStreamEvent(CFReadStreamRef stream, CFStreamEventType type, void *clientCallBackInfo)
{
HTTPSCertificateRetriever *self = (__bridge HTTPSCertificateRetriever *)clientCallBackInfo;
[self handleStreamEvent:type];
}
@implementation HTTPSCertificateRetriever
- (instancetype)initWithURL:(NSURL*)url
delegate:(id<HTTPSCertificateRetrieverDelegate>)delegate
{
self = [super init];
if (self)
{
if (![[[url scheme] lowercaseString] isEqualToString:@"https"])
return nil;
self.delegate = delegate;
self.request = CFBridgingRelease(CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)url, kCFHTTPVersion1_0));
}
return self;
}
- (void)dealloc
{
[self closeStream];
}
- (CFReadStreamRef)cfReadStream
{
return (__bridge CFReadStreamRef)self.readStream;
}
- (void)startWithTimeout:(NSTimeInterval)timeout
retries:(NSInteger)retries
{
if (self.readStream)
[self stop];
self.hasCompleted = NO;
self.certificate = nil;
if (timeout > 0.0)
{
self.timeoutTimer = [NSTimer timerWithTimeInterval:timeout
block:^{
[self closeStream];
if (retries > 0)
{
[self startWithTimeout:timeout
retries:(retries - 1)];
}
else
{
self.hasCompleted = YES;
[self.delegate certificateRetrieved:nil];
}
}
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.timeoutTimer
forMode:NSRunLoopCommonModes];
}
else
{
if ([self.timeoutTimer isValid])
{
[self.timeoutTimer invalidate];
self.timeoutTimer = nil;
}
}
self.readStream = CFBridgingRelease(CFReadStreamCreateForHTTPRequest(NULL, (__bridge CFHTTPMessageRef)self.request));
CFStreamClientContext context = { 0, (__bridge void *)(self), NULL, NULL, NULL };
CFOptionFlags streamEvents = kCFStreamEventHasBytesAvailable | kCFStreamEventEndEncountered | kCFStreamEventErrorOccurred;
CFReadStreamSetClient(self.cfReadStream, streamEvents, bridgeStreamEvent, &context);
/* We set these settings so we'll accept self-signed certs as well as expired certificates. */
NSDictionary *sslSettings = @{
(NSString*)kCFStreamSSLAllowsAnyRoot: @YES,
(NSString*)kCFStreamSSLValidatesCertificateChain: @NO,
(NSString*)kCFStreamSSLAllowsExpiredCertificates: @YES,
(NSString*)kCFStreamSSLAllowsExpiredRoots: @YES,
};
CFReadStreamSetProperty(self.cfReadStream, kCFStreamPropertySSLSettings, (__bridge CFTypeRef)(sslSettings));
CFReadStreamScheduleWithRunLoop(self.cfReadStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
CFReadStreamOpen(self.cfReadStream);
}
- (void)stop
{
[self closeStream];
}
- (void)closeStream
{
if ([self.timeoutTimer isValid])
{
[self.timeoutTimer invalidate];
self.timeoutTimer = nil;
}
if (self.readStream)
{
CFReadStreamClose(self.cfReadStream);
CFReadStreamSetClient(self.cfReadStream, kCFStreamEventNone, NULL, NULL);
CFReadStreamUnscheduleFromRunLoop(self.cfReadStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
self.readStream = nil;
}
}
- (void)handleStreamEvent:(CFStreamEventType)event
{
NSArray *certs = [self.readStream propertyForKey:(NSString*)kCFStreamPropertySSLPeerCertificates];
SecCertificateRef certificateRef = NULL;
if ([certs count] > 0)
certificateRef = (__bridge SecCertificateRef)([certs objectAtIndex:0]);
if (certificateRef)
self.certificate = CFBridgingRelease(SecCertificateCopyData(certificateRef));
else
self.certificate = nil;
[self closeStream];
self.hasCompleted = YES;
[self.delegate certificateRetrieved:self.certificate];
}
@end
#import <Foundation/Foundation.h>
@interface NSTimer (Blocks)
+ (id)scheduledTimerWithTimeInterval:(NSTimeInterval)aTimeInterval
block:(void (^)())aBlock
repeats:(BOOL)repeats;
+ (id)timerWithTimeInterval:(NSTimeInterval)aTimeInterval
block:(void (^)())aBlock
repeats:(BOOL)repeats;
@end
#import "NSTimer+Blocks.h"
@implementation NSTimer (Blocks)
+ (id)scheduledTimerWithTimeInterval:(NSTimeInterval)aTimeInterval
block:(void (^)())aBlock
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:aTimeInterval
target:self
selector:@selector(executeBlock:)
userInfo:[aBlock copy]
repeats:repeats];
}
+ (id)timerWithTimeInterval:(NSTimeInterval)aTimeInterval
block:(void (^)())aBlock
repeats:(BOOL)repeats
{
return [self timerWithTimeInterval:aTimeInterval
target:self
selector:@selector(executeBlock:)
userInfo:[aBlock copy]
repeats:repeats];
}
+ (void)executeBlock:(NSTimer *)aTimer;
{
if ([aTimer userInfo])
{
void (^block)() = (void (^)())[aTimer userInfo];
block();
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment