注册

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

这3个key的大意如注释,后面我们还会讲到,其中最重要的是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中去,并且标记当前的状态,然后去执行maybeDequeueReadmaybeDequeueWrite
需要注意的是,这里只有读到这个GCDAsyncSpecialPacket时,才开始TLS认证和握手。

接着我们就来到了maybeDequeueRead这个方法,这个方法我们在前面第一条中讲到过,忘了的可以往上拉一下页面就可以看到。
它就是让我们的ReadQueue中的读任务离队,并且开始执行这条读任务。

  • 当我们读到的是GCDAsyncSpecialPacket类型的包,则开始进行TLS认证。
  • 当我们读到的是GCDAsyncReadPacket类型的包,则开始进行一次读取数据的任务。
  • 如果ReadQueue为空,则对几种情况进行判断,是否是读取上一次数据失败,则断开连接。
    如果是基于TLSSocket,则把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,取得配置字典中keyGCDAsyncSocketUseCFStreamForTLS的值:
如果为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握手做准备。











0 个评论

要回复文章请先登录注册