CocoaAsyncSocket源码Read(二)
讲讲两种TLS建立连接的过程
讲到这里,就不得不提一下,这里个框架开启TLS
的过程。它对外提供了这么一个方法来开启TLS
:
- (void)startTLS:(NSDictionary *)tlsSettings
可以根据一个字典,去开启并且配置TLS
,那么这个字典里包含什么内容呢?
一共包含以下这些key
:
//配置SSL上下文的设置
// Configure SSLContext from given settings
//
// Checklist:
// 1\. kCFStreamSSLPeerName //证书名
// 2\. kCFStreamSSLCertificates //证书数组
// 3\. GCDAsyncSocketSSLPeerID //证书ID
// 4\. GCDAsyncSocketSSLProtocolVersionMin //SSL最低版本
// 5\. GCDAsyncSocketSSLProtocolVersionMax //SSL最高版本
// 6\. GCDAsyncSocketSSLSessionOptionFalseStart
// 7\. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
// 8\. GCDAsyncSocketSSLCipherSuites
// 9\. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
//
// Deprecated (throw error): //被废弃的参数,如果设置了就会报错关闭socket
// 10\. kCFStreamSSLAllowsAnyRoot
// 11\. kCFStreamSSLAllowsExpiredRoots
// 12\. kCFStreamSSLAllowsExpiredCertificates
// 13\. kCFStreamSSLValidatesCertificateChain
// 14\. kCFStreamSSLLevel
Key
的值,具体是什么意思,value如何设置,可以查查苹果文档,限于篇幅,我们就不赘述了,只需要了解重要的几个参数即可。后面一部分是被废弃的参数,如果我们设置了,就会报错关闭
socket
连接。除此之外,还有这么3个
key
被我们遗漏了,这3个key,是框架内部用来判断,并且做一些处理的标识:kCFStreamSSLIsServer //判断当前是否是服务端
GCDAsyncSocketManuallyEvaluateTrust //判断是否需要手动信任SSL
GCDAsyncSocketUseCFStreamForTLS //判断是否使用CFStream形式的TLS
GCDAsyncSocketUseCFStreamForTLS
这个key
,一旦我们设置为YES,将开启CFStream
的TLS,关于这种基于流的TLS
与普通的TLS
的区别,我们来看看官方说明:- GCDAsyncSocketUseCFStreamForTLS (iOS only)
The value must be of type NSNumber, encapsulating a BOOL value.
By default GCDAsyncSocket will use the SecureTransport layer to perform encryption.
This gives us more control over the security protocol (many more configuration options),
plus it allows us to optimize things like sys calls and buffer allocation.
However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption
technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket
will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property
(via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method.
Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket,
and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty.
For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings.
If unspecified, the default value is NO.
从上述说明中,我们可以得知,CFStream
形式的TLS
仅仅可以被用于iOS
平台,并且它是一种过时的加解密技术,如果我们没有必要,最好还是不要用这种方式的TLS
。
至于它的实现,我们接着往下看。
//开启TLS
- (void)startTLS:(NSDictionary *)tlsSettings
{
LogTrace();
if (tlsSettings == nil)
{
tlsSettings = [NSDictionary dictionary];
}
//新生成一个TLS特殊的包
GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
dispatch_async(socketQueue, ^{ @autoreleasepool {
if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
{
//添加到读写Queue中去
[readQueue addObject:packet];
[writeQueue addObject:packet];
//把TLS标记加上
flags |= kQueuedTLS;
//开始读取TLS的任务,读到这个包会做TLS认证。在这之前的包还是不用认证就可以传送完
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
}});
}
这个方法就是对外提供的开启TLS
的方法,它把传进来的字典,包成一个TLS的特殊包,这个GCDAsyncSpecialPacket
类包里面就一个字典属性:
- (id)initWithTLSSettings:(NSDictionary *)settings;
然后我们把这个包添加到读写queue
中去,并且标记当前的状态,然后去执行maybeDequeueRead
或maybeDequeueWrite
。
需要注意的是,这里只有读到这个GCDAsyncSpecialPacket
时,才开始TLS认证和握手。
接着我们就来到了maybeDequeueRead
这个方法,这个方法我们在前面第一条中讲到过,忘了的可以往上拉一下页面就可以看到。
它就是让我们的ReadQueue
中的读任务离队,并且开始执行这条读任务。
- 当我们读到的是
GCDAsyncSpecialPacket
类型的包,则开始进行TLS认证。 - 当我们读到的是
GCDAsyncReadPacket
类型的包,则开始进行一次读取数据的任务。 - 如果
ReadQueue
为空,则对几种情况进行判断,是否是读取上一次数据失败,则断开连接。
如果是基于TLS
的Socket
,则把SSL
安全通道的数据,移到全局缓冲区preBuffer
中。如果数据仍然为空,则恢复读source
,等待下一次读source
的触发。
接着我们来看看这其中第一条,当读到的是一个GCDAsyncSpecialPacket
类型的包,我们会调用maybeStartTLS
这个方法:
//可能开启TLS
- (void)maybeStartTLS
{
//只有读和写TLS都开启
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
//需要安全传输
BOOL useSecureTransport = YES;
#if TARGET_OS_IPHONE
{
//拿到当前读的数据
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
//得到设置字典
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
//拿到Key为CFStreamTLS的 value
NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];
if (value && [value boolValue])
//如果是用CFStream的,则安全传输为NO
useSecureTransport = NO;
}
#endif
//如果使用安全通道
if (useSecureTransport)
{
//开启TLS
[self ssl_startTLS];
}
//CFStream形式的Tls
else
{
#if TARGET_OS_IPHONE
[self cf_startTLS];
#endif
}
}
}
这里根据我们之前添加标记,判断是否读写TLS状态,是才继续进行接下来的TLS
认证。
接着我们拿到当前GCDAsyncSpecialPacket
,取得配置字典中key
为GCDAsyncSocketUseCFStreamForTLS
的值:
如果为YES
则说明使用CFStream
形式的TLS
,否则使用SecureTransport
安全通道形式的TLS
。关于这个配置项,还有二者的区别,我们前面就讲过了。
接着我们分别来看看这两个方法,先来看看ssl_startTLS
。
这个方法非常长,大概有400多行,所以为了篇幅和大家阅读体验,楼主简化了一部分内容用省略号+注释的形式表示。
//开启TLS
- (void)ssl_startTLS
{
LogTrace();
LogVerbose(@"Starting TLS (via SecureTransport)...");
//状态标记
OSStatus status;
//拿到当前读的数据包
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
if (tlsPacket == nil) // Code to quiet the analyzer
{
NSAssert(NO, @"Logic error");
[self closeWithError:[self otherError:@"Logic error"]];
return;
}
//拿到设置
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
// Create SSLContext, and setup IO callbacks and connection ref
//根据key来判断,当前包是否是服务端的
BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue];
//创建SSL上下文
#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
{
//如果是服务端的创建服务端上下文,否则是客户端的上下文,用stream形式
if (isServer)
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
else
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
//为空则报错返回
if (sslContext == NULL)
{
[self closeWithError:[self otherError:@"Error in SSLCreateContext"]];
return;
}
}
#else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
{
status = SSLNewContext(isServer, &sslContext);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLNewContext"]];
return;
}
}
#endif
//给SSL上下文设置 IO回调 分别为SSL 读写函数
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
//设置出错
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
return;
}
//在握手之调用,建立SSL连接 ,第一次连接 1
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
//连接出错
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
return;
}
//是否应该手动的去信任SSL
BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue];
//如果需要手动去信任
if (shouldManuallyEvaluateTrust)
{
//是服务端的话,不需要,报错返回
if (isServer)
{
[self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]];
return;
}
//第二次连接 再去连接用kSSLSessionOptionBreakOnServerAuth的方式,去连接一次,这种方式可以直接信任服务端证书
status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
//错误直接返回
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]];
return;
}
#if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
// Note from Apple's documentation:
//
// It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8.
// On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the
// built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus
// SSLSetEnableCertVerify is not available on that platform at all.
//为了防止kSSLSessionOptionBreakOnServerAuth这种情况下,产生了不受信任的环境
status = SSLSetEnableCertVerify(sslContext, NO);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
return;
}
#endif
}
//配置SSL上下文的设置
id value;
//这个参数是用来获取证书名验证,如果设置为NULL,则不验证
// 1\. kCFStreamSSLPeerName
value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName];
if ([value isKindOfClass:[NSString class]])
{
NSString *peerName = (NSString *)value;
const char *peer = [peerName UTF8String];
size_t peerLen = strlen(peer);
//把证书名设置给SSL
status = SSLSetPeerDomainName(sslContext, peer, peerLen);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]];
return;
}
}
//不是string就错误返回
else if (value)
{
//这个断言啥用也没有啊。。
NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString.");
[self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]];
return;
}
// 2\. kCFStreamSSLCertificates
...
// 3\. GCDAsyncSocketSSLPeerID
...
// 4\. GCDAsyncSocketSSLProtocolVersionMin
...
// 5\. GCDAsyncSocketSSLProtocolVersionMax
...
// 6\. GCDAsyncSocketSSLSessionOptionFalseStart
...
// 7\. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
...
// 8\. GCDAsyncSocketSSLCipherSuites
...
// 9\. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
...
//弃用key的检查,如果有下列key对应的value,则都报弃用的错误
// 10\. kCFStreamSSLAllowsAnyRoot
...
// 11\. kCFStreamSSLAllowsExpiredRoots
...
// 12\. kCFStreamSSLAllowsExpiredCertificates
...
// 13\. kCFStreamSSLValidatesCertificateChain
...
// 14\. kCFStreamSSLLevel
...
// Setup the sslPreBuffer
//
// Any data in the preBuffer needs to be moved into the sslPreBuffer,
// as this data is now part of the secure read stream.
//初始化SSL提前缓冲 也是4Kb
sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
//获取到preBuffer可读大小
size_t preBufferLength = [preBuffer availableBytes];
//如果有可读内容
if (preBufferLength > 0)
{
//确保SSL提前缓冲的大小
[sslPreBuffer ensureCapacityForWrite:preBufferLength];
//从readBuffer开始读,读这个长度到 SSL提前缓冲的writeBuffer中去
memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength);
//移动提前的读buffer
[preBuffer didRead:preBufferLength];
//移动sslPreBuffer的写buffer
[sslPreBuffer didWrite:preBufferLength];
}
//拿到上次错误的code,并且让上次错误code = 没错
sslErrCode = lastSSLHandshakeError = noErr;
// Start the SSL Handshake process
//开始SSL握手过程
[self ssl_continueSSLHandshake];
}
这个方法的结构也很清晰,主要就是建立TLS
连接,并且配置SSL
上下文对象:sslContext
,为TLS
握手做准备。