注册

CocoaAsyncSocket源码Read(三)

这里我们就讲讲几个重要的关于SSL的函数,其余细节可以看看注释:

  1. 创建SSL上下文对象:
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);

这个函数用来创建一个SSL上下文,我们接下来会把配置字典tlsSettings中所有的参数,都设置到这个sslContext中去,然后用这个sslContext进行TLS后续操作,握手等。

  1. 给SSL设置读写回调:
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);

这两个回调函数如下:

//读函数
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
{
//拿到socket
GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;

//断言当前为socketQueue
NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");

//读取数据,并且返回状态码
return [asyncSocket sslReadWithBuffer:data length:dataLength];
}
//写函数
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
{
GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;

NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");

return [asyncSocket sslWriteWithBuffer:data length:dataLength];
}

他们分别调用了sslReadWithBuffersslWriteWithBuffer两个函数进行SSL的读写处理,关于这两个函数,我们后面再来说。
  1. 发起SSL连接:
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);

到这一步,前置的重要操作就完成了,接下来我们是对SSL进行一些额外的参数配置:
我们根据tlsSettingsGCDAsyncSocketManuallyEvaluateTrust字段,去判断是否需要手动信任服务端证书,调用如下函数

status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);

这个函数是用来设置一些可选项的,当然不止kSSLSessionOptionBreakOnServerAuth这一种,还有许多种类型的可选项,感兴趣的朋友可以自行点进去看看这个枚举。

接着我们按照字典中的设置项,一项一项去设置ssl上下文,类似:


status = SSLSetPeerDomainName(sslContext, peer, peerLen);

设置完这些有效的,我们还需要去检查无效的key,万一我们设置了这些废弃的api,我们需要报错处理。

做完这些操作后,我们初始化了一个sslPreBuffer,这个ssl安全通道下的全局缓冲区:

sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];

然后把prebuffer全局缓冲区中的数据全部挪到sslPreBuffer中去,这里为什么要这么做呢?按照我们上面的流程图来说,正确的数据流向应该是从sslPreBuffer->prebuffer的,楼主在这里也思考了很久,最后我的想法是,就是初始化的时候,数据的流向的统一,在我们真正数据读取的时候,就不需要做额外的判断了。

到这里我们所有的握手前初始化工作都做完了。

接着我们调用了ssl_continueSSLHandshake方法开始SSL握手

//SSL的握手
- (void)ssl_continueSSLHandshake
{
LogTrace();

//用我们的SSL上下文对象去握手
OSStatus status = SSLHandshake(sslContext);
//拿到握手的结果,赋值给上次握手的结果
lastSSLHandshakeError = status;

//如果没错
if (status == noErr)
{
LogVerbose(@"SSLHandshake complete");

//把开始读写TLS,从标记中移除
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;

//把Socket安全通道标记加上
flags |= kSocketSecure;

//拿到代理
__strong id theDelegate = delegate;

if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)])
{
dispatch_async(delegateQueue, ^{ @autoreleasepool {
//调用socket已经开启安全通道的代理方法
[theDelegate socketDidSecure:self];
}});
}
//停止读取
[self endCurrentRead];
//停止写
[self endCurrentWrite];
//开始下一次读写任务
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
//如果是认证错误
else if (status == errSSLPeerAuthCompleted)
{
LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval");

__block SecTrustRef trust = NULL;
//从sslContext拿到证书相关的细节
status = SSLCopyPeerTrust(sslContext, &trust);
//SSl证书赋值出错
if (status != noErr)
{
[self closeWithError:[self sslError:status]];
return;
}

//拿到状态值
int aStateIndex = stateIndex;
//socketQueue
dispatch_queue_t theSocketQueue = socketQueue;

__weak GCDAsyncSocket *weakSelf = self;

//创建一个完成Block
void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"

dispatch_async(theSocketQueue, ^{ @autoreleasepool {

if (trust) {
CFRelease(trust);
trust = NULL;
}

__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf)
{
[strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex];
}
}});

#pragma clang diagnostic pop
}};

__strong id theDelegate = delegate;

if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)])
{
dispatch_async(delegateQueue, ^{ @autoreleasepool {

#pragma mark - 调用代理我们自己去https认证
[theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];
}});
}
//没实现代理直接报错关闭连接。
else
{
if (trust) {
CFRelease(trust);
trust = NULL;
}

NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings,"
@" but delegate doesn't implement socket:shouldTrustPeer:";

[self closeWithError:[self otherError:msg]];
return;
}
}

//握手错误为 IO阻塞的
else if (status == errSSLWouldBlock)
{
LogVerbose(@"SSLHandshake continues...");

// Handshake continues...
//
// This method will be called again from doReadData or doWriteData.
}
else
{
//其他错误直接关闭连接
[self closeWithError:[self sslError:status]];
}
}

这个方法就做了一件事,就是SSL握手,我们调用了这个函数完成握手:


OSStatus status = SSLHandshake(sslContext);

然后握手的结果分为4种情况:

  1. 如果返回为noErr,这个会话已经准备好了安全的通信,握手成功。
  • 如果返回的valueerrSSLWouldBlock,握手方法必须再次调用。
  • 如果返回为errSSLServerAuthCompleted,如果我们要调用代理,我们需要相信服务器,然后再次调用握手,去恢复握手或者关闭连接。
  • 否则,返回的value表明了错误的code

其中需要说说的是errSSLWouldBlock,这个是IO阻塞下的错误,也就是服务器的结果还没来得及返回,当握手结果返回的时候,这个方法会被再次触发。

还有就是errSSLServerAuthCompleted下,我们回调了代理:

[theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];

我们可以去手动对证书进行认证并且信任,当完成回调后,会调用到这个方法里来,再次进行握手:

//修改信息后再次进行SSL握手
- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex
{
LogTrace();

if (aStateIndex != stateIndex)
{
return;
}

// Increment stateIndex to ensure completionHandler can only be called once.
stateIndex++;

if (shouldTrust)
{
NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError);
[self ssl_continueSSLHandshake];
}
else
{

[self closeWithError:[self sslError:errSSLPeerBadCert]];
}
}



到这里,我们就整个完成安全通道下的TLS认证。

接着我们来看看基于CFStreamTLS

因为CFStream是上层API,所以它的TLS流程相当简单,我们来看看cf_startTLS这个方法:


//CF流形式的TLS
- (void)cf_startTLS
{
LogTrace();

LogVerbose(@"Starting TLS (via CFStream)...");

//如果preBuffer的中可读数据大于0,错误关闭
if ([preBuffer availableBytes] > 0)
{
NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket.";

[self closeWithError:[self otherError:msg]];
return;
}

//挂起读写source
[self suspendReadSource];
[self suspendWriteSource];

//把未读的数据大小置为0
socketFDBytesAvailable = 0;
//去掉下面两种flag
flags &= ~kSocketCanAcceptBytes;
flags &= ~kSecureSocketHasBytesAvailable;

//标记为CFStream
flags |= kUsingCFStreamForTLS;

//如果创建读写stream失败
if (![self createReadAndWriteStream])
{
[self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
return;
}
//注册回调,这回监听可读数据了!!
if (![self registerForStreamCallbacksIncludingReadWrite:YES])
{
[self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
return;
}
//添加runloop
if (![self addStreamsToRunLoop])
{
[self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
return;
}

NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS");
NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS");

//拿到当前包
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
//拿到ssl配置
CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;

// Getting an error concerning kCFStreamPropertySSLSettings ?
// You need to add the CFNetwork framework to your iOS application.

//直接设置给读写stream
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);

//设置失败
if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug.
{
[self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]];
return;
}

//打开流
if (![self openStreams])
{
[self closeWithError:[self otherError:@"Error in CFStreamOpen"]];
return;
}

LogVerbose(@"Waiting for SSL Handshake to complete...");
}
1.这个方法很简单,首先它挂起了读写source,然后重新初始化了读写流,并且绑定了回调,和添加了runloop
这里我们为什么要用重新这么做?看过之前connect篇的同学就知道,我们在连接成功之后,去初始化过读写流,这些操作之前都做过。而在这里重新初始化,并不会重新创建,只是修改读写流的一些参数,其中主要是下面这个方法,传递了一个YES过去:
if (![self registerForStreamCallbacksIncludingReadWrite:YES])

这个参数会使方法里多添加一种触发回调的方式:kCFStreamEventHasBytesAvailable
当有数据可读时候,触发Stream回调。

2.接着我们用下面这个函数把TLS的配置参数,设置给读写stream:

//直接设置给读写stream
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);

3.最后打开读写流,整个CFStream形式的TLS就完成了。

看到这,大家可能对数据触发的问题有些迷惑。总结一下,我们到现在一共有3种触发的回调:
  1. 读写source:这个和socket绑定在一起,一旦有数据到达,就会触发事件句柄,但是我们可以看到在cf_startTLS方法中我们调用了:

 //挂起读写source
[self suspendReadSource];
[self suspendWriteSource];

所以,对于CFStream形式的TLS的读写并不是由source触发的,而其他的都是由source来触发。

  1. CFStream绑定的几种事件的读写回调函数:
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)

这个和CFStream形式的TLS相关,会触发这种形式的握手,流末尾等出现的错误,还有该形式下数据到达。
因为我们在一开始的连接完成就初始化过stream,所以非CFStream形式下也回触发这个回调,只是不会在数据到达触发而已。

  1. SSL安全通道形式,绑定的SSL读写函数:
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)

这个函数并不是由系统触发,而是需要我们主动去调用SSLReadSSLWrite两个函数,回调才能被触发。























0 个评论

要回复文章请先登录注册