kSocketSecure = 1 << 13, // If set, socket is using secure communication
kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport
在标识CocoaAsyncSocket
的状态的GCDAsyncSocketFlags
内有这样两个数值.不是说好的CFStream for TLS
和SecureTransport for TLS
.为什么没有kUsingSecureTransportForTLS
?
kSocketSecure
包含了kUsingCFStreamForTLS
和kUsingSecureTransportForTLS
;判断用什么方式实现TLS是用以下两个方法:
- (BOOL)usingCFStreamForTLS
{
#if TARGET_OS_IPHONE
if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS))
{
// The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag.
return YES;
}
#endif
return NO;
}
- (BOOL)usingSecureTransportForTLS
{
// Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable)
#if TARGET_OS_IPHONE
if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS))
{
// The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag.
return NO;
}
#endif
return YES;
}
SecureTransport for TLS
用到了系统的Secure.framework
的内容.
开启SecureTransport for TLS
代码如下:
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
if(![_socket connectToHost:@"127.0.0.1" onPort:443 withTimeout:30 error:nil]){
NSLog(@"连接失败");
}else{
NSMutableDictionary *settings = [[NSMutableDictionary alloc] init];
[settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust];
[_socket startTLS:settings];
[_socket readDataWithTimeout:-1 tag:110];
}
开启CFStream for TLS
代码如下:
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
if(![_socket connectToHost:@"127.0.0.1" onPort:443 withTimeout:30 error:nil]){
NSLog(@"连接失败");
}else{
NSMutableDictionary *settings = [[NSMutableDictionary alloc] init];
[settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust];
[settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketUseCFStreamForTLS];
[_socket startTLS:settings];
[_socket readDataWithTimeout:-1 tag:110];
}
调用方法startTLS:
,传入参数字典就开启了TLS.
- (void)startTLS:(NSDictionary *)tlsSettings
{
LogTrace();
if (tlsSettings == nil)
{
// Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary,
// but causes problems if we later try to fetch the remote host's certificate.
//
// To be exact, it causes the following to return NULL instead of the normal result:
// CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates)
//
// So we use an empty dictionary instead, which works perfectly.
tlsSettings = [NSDictionary dictionary];
}
GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
dispatch_async(socketQueue, ^{ @autoreleasepool {
if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[writeQueue addObject:packet];
flags |= kQueuedTLS;
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
}});
}
利用传入的字典创建一个GCDAsyncSpecialPacket
.(与GCDAsyncSpecialPacket
对应的是GCDAsyncWritePacket
和GCDAsyncReadPacket
.GCDAsyncSpecialPacket
是专门用于开启TLS的;GCDAsyncReadPacket
和GCDAsyncWritePacket
是做读写用的)
方法- (void)maybeStartTLS
这里分水岭,外围传入的字典如果设置了GCDAsyncSocketUseCFStreamForTLS
对YES
,则走[self cf_startTLS]
,否则就走[self ssl_startTLS]
.
1.cf_startTLS
CocoaAsyncSocket
创建与连接的过程中两个CFStream
早就已经建好.
但是还记得我们刚刚说的吗?不开启CF for TLS
,两个CFStream
是没有监听作用的.而调用cf_startTLS
后,会又调用registerForStreamCallbacksIncludingReadWrite:
传入YES
.
[self registerForStreamCallbacksIncludingReadWrite:YES]
打开CFStream
对可读数据和可用空间的监听.
我们通过- (void)startTLS:(NSDictionary *)tlsSettings;
传入的字典,字典会赋给两个CFStream
.
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);
2.ssl_startTLS
走ssl_startTLS
会对应有:
SSLContextRef sslContext;
我们通过- (void)startTLS:(NSDictionary *)tlsSettings;
传入的字典,字典的每个键值对
会挨个赋给SSLContextRef sslContext
.
SSLContextRef sslContext
的属性很多,有些我也不清楚具体的用途,想弄明白的请自行看文档:
// Configure SSLContext from given settings
//
// Checklist:
// 1. kCFStreamSSLPeerName
// 2. kCFStreamSSLCertificates
// 3. GCDAsyncSocketSSLPeerID
// 4. GCDAsyncSocketSSLProtocolVersionMin
// 5. GCDAsyncSocketSSLProtocolVersionMax
// 6. GCDAsyncSocketSSLSessionOptionFalseStart
// 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
// 8. GCDAsyncSocketSSLCipherSuites
// 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
//
// Deprecated (throw error):
// 10. kCFStreamSSLAllowsAnyRoot
// 11. kCFStreamSSLAllowsExpiredRoots
// 12. kCFStreamSSLAllowsExpiredCertificates
// 13. kCFStreamSSLValidatesCertificateChain
// 14. kCFStreamSSLLevel
配置完SSLContextRef sslContext
后,调用- (void)ssl_continueSSLHandshake;
- (void)ssl_continueSSLHandshake;
方法内部会调用OSStatus status = SSLHandshake(sslContext);
得到的status
决定了下一步怎么走.
status:
noErr==>握手成功
errSSLPeerAuthCompleted==>将SecTrustRef传给代理,征求代理的意见
errSSLWouldBlock==>出现拥塞,这时socket读写已经打开,所以在读与写的时候都有可能再次调用ssl_continueSSLHandshake
其他==>报错
GCDAsyncSocket for TLS.png
<<开启TLS总结>>
开启TLS的所有步骤可以总结为上面的流程图.需要注意的是,不开启TLS
的数据读写与SecureTransport for TLS
的数据读写都走dispatch_source
对bsd socket
的监听,而CFStream for TLS
的数据读写则用的是CFStream
自己对内部bsd socket
的监听.
无论用什么方式监听bsd socket
,一旦监听到有数据可读,或者有空间可以写入,都会调用对应方法- (void)doReadData
或者- (void)doWriteData
.
网友评论