这里不对https和双向认证做理论解释,直接写具体的操作.
问题分析 :
RN中的网络部分都得进行证书认证处理
- Fetch
- react-native-fetch-blob
- webview
这三者都得单独对对应的ios和android源码进行处理,这篇文章先讲ios的.
首先将client.p12(认证客户端用的)server.cer(服务端的自签名证书,ios这边我没用它)导入工程里
一 : Fetch
27BF2A9B-BF9D-4CE4-BD04-7A015DE8D044.png/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTHTTPRequestHandler.h"
#import <mutex>
#import "RCTNetworking.h"
@interface RCTHTTPRequestHandler () <NSURLSessionDataDelegate>
@end
@implementation RCTHTTPRequestHandler
{
NSMapTable *_delegates;
NSURLSession *_session;
std::mutex _mutex;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
- (void)invalidate
{
[_session invalidateAndCancel];
_session = nil;
}
- (BOOL)isValid
{
// if session == nil and delegates != nil, we've been invalidated
return _session || !_delegates;
}
#pragma mark - NSURLRequestHandler
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
static NSSet<NSString *> *schemes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// technically, RCTHTTPRequestHandler can handle file:// as well,
// but it's less efficient than using RCTFileRequestHandler
schemes = [[NSSet alloc] initWithObjects:@"http", @"https", nil];
});
return [schemes containsObject:request.URL.scheme.lowercaseString];
}
- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request
withDelegate:(id<RCTURLRequestDelegate>)delegate
{
// Lazy setup
if (!_session && [self isValid]) {
NSOperationQueue *callbackQueue = [NSOperationQueue new];
callbackQueue.maxConcurrentOperationCount = 1;
callbackQueue.underlyingQueue = [[_bridge networking] methodQueue];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:callbackQueue];
std::lock_guard<std::mutex> lock(_mutex);
_delegates = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
valueOptions:NSPointerFunctionsStrongMemory
capacity:0];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
{
std::lock_guard<std::mutex> lock(_mutex);
[_delegates setObject:delegate forKey:task];
}
[task resume];
return task;
}
- (void)cancelRequest:(NSURLSessionDataTask *)task
{
{
std::lock_guard<std::mutex> lock(_mutex);
[_delegates removeObjectForKey:task];
}
[task cancel];
}
#pragma mark - NSURLSession delegate
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
}
[delegate URLRequest:task didSendDataWithProgress:totalBytesSent];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
}
[delegate URLRequest:task didReceiveResponse:response];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveData:(NSData *)data
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
}
[delegate URLRequest:task didReceiveData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
[_delegates removeObjectForKey:task];
}
[delegate URLRequest:task didCompleteWithError:error];
}
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
//挑战处理类型为 默认
/*
NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
NSURLSessionAuthChallengeUseCredential:使用指定的证书
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
*/
__weak typeof(self) weakSelf = self;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// sessionDidReceiveAuthenticationChallenge是自定义方法,用来如何应对服务器端的认证挑战
// 而这个证书就需要使用credentialForTrust:来创建一个NSURLCredential对象
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 创建挑战证书(注:挑战方式为UseCredential和PerformDefaultHandling都需要新建挑战证书)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 确定挑战的方式
if (credential) {
//证书挑战 设计policy,none,则跑到这里
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
// client authentication
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSFileManager *fileManager =[NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];
if ([[weakSelf class]extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
{
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void*certs[] = {certificate};
CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
credential =[NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
disposition =NSURLSessionAuthChallengeUseCredential;
}
}
}
//完成挑战
if (completionHandler) {
completionHandler(disposition, credential);
}
}
+(BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityError = errSecSuccess;
//client certificate password
NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"cplh123456"
forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge CFDictionaryRef)optionsDictionary,&items);
if(securityError == 0) {
CFDictionaryRef myIdentityAndTrust = (CFDictionaryRef)CFArrayGetValueAtIndex(items,0);
const void*tempIdentity =NULL;
tempIdentity= CFDictionaryGetValue (myIdentityAndTrust,kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void*tempTrust =NULL;
tempTrust = CFDictionaryGetValue(myIdentityAndTrust,kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failedwith error code %d",(int)securityError);
return NO;
}
return YES;
}
@end
二 : react-native-fetch-blob
28ACF9B1-E9B8-4BEF-8CA1-9B3EFF61CD6A.png//
// RNFetchBlobNetwork.m
// RNFetchBlob
//
// Created by wkh237 on 2016/6/6.
// Copyright © 2016 wkh237. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "RNFetchBlob.h"
#import "RNFetchBlobFS.h"
#import "RNFetchBlobNetwork.h"
#import "RNFetchBlobConst.h"
#import "RNFetchBlobReqBuilder.h"
#import "IOS7Polyfill.h"
#import <CommonCrypto/CommonDigest.h>
#import "RNFetchBlobProgress.h"
#if __has_include(<React/RCTAssert.h>)
#import <React/RCTRootView.h>
#import <React/RCTLog.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTBridge.h>
#else
#import "RCTRootView.h"
#import "RCTLog.h"
#import "RCTEventDispatcher.h"
#import "RCTBridge.h"
#endif
////////////////////////////////////////
//
// HTTP request handler
//
////////////////////////////////////////
NSMapTable * taskTable;
NSMapTable * expirationTable;
NSMutableDictionary * progressTable;
NSMutableDictionary * uploadProgressTable;
__attribute__((constructor))
static void initialize_tables() {
if(expirationTable == nil)
{
expirationTable = [[NSMapTable alloc] init];
}
if(taskTable == nil)
{
taskTable = [[NSMapTable alloc] init];
}
if(progressTable == nil)
{
progressTable = [[NSMutableDictionary alloc] init];
}
if(uploadProgressTable == nil)
{
uploadProgressTable = [[NSMutableDictionary alloc] init];
}
}
typedef NS_ENUM(NSUInteger, ResponseFormat) {
UTF8,
BASE64,
AUTO
};
@interface RNFetchBlobNetwork ()
{
BOOL * respFile;
BOOL isNewPart;
BOOL * isIncrement;
NSMutableData * partBuffer;
NSString * destPath;
NSOutputStream * writeStream;
long bodyLength;
NSMutableDictionary * respInfo;
NSInteger respStatus;
NSMutableArray * redirects;
ResponseFormat responseFormat;
BOOL * followRedirect;
BOOL backgroundTask;
}
@end
@implementation RNFetchBlobNetwork
NSOperationQueue *taskQueue;
@synthesize taskId;
@synthesize expectedBytes;
@synthesize receivedBytes;
@synthesize respData;
@synthesize callback;
@synthesize bridge;
@synthesize options;
@synthesize fileTaskCompletionHandler;
@synthesize dataTaskCompletionHandler;
@synthesize error;
// constructor
- (id)init {
self = [super init];
if(taskQueue == nil) {
taskQueue = [[NSOperationQueue alloc] init];
taskQueue.maxConcurrentOperationCount = 10;
}
return self;
}
+ (void) enableProgressReport:(NSString *) taskId config:(RNFetchBlobProgress *)config
{
if(progressTable == nil)
{
progressTable = [[NSMutableDictionary alloc] init];
}
[progressTable setValue:config forKey:taskId];
}
+ (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *)config
{
if(uploadProgressTable == nil)
{
uploadProgressTable = [[NSMutableDictionary alloc] init];
}
[uploadProgressTable setValue:config forKey:taskId];
}
// removing case from headers
+ (NSMutableDictionary *) normalizeHeaders:(NSDictionary *)headers
{
NSMutableDictionary * mheaders = [[NSMutableDictionary alloc]init];
for(NSString * key in headers) {
[mheaders setValue:[headers valueForKey:key] forKey:[key lowercaseString]];
}
return mheaders;
}
- (NSString *)md5:(NSString *)input {
const char* str = [input UTF8String];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), result);
NSMutableString *ret = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH*2];
for(int i = 0; i<CC_MD5_DIGEST_LENGTH; i++) {
[ret appendFormat:@"%02x",result[I]];
}
return ret;
}
// send HTTP request
- (void) sendRequest:(__weak NSDictionary * _Nullable )options
contentLength:(long) contentLength
bridge:(RCTBridge * _Nullable)bridgeRef
taskId:(NSString * _Nullable)taskId
withRequest:(__weak NSURLRequest * _Nullable)req
callback:(_Nullable RCTResponseSenderBlock) callback
{
self.taskId = taskId;
self.respData = [[NSMutableData alloc] initWithLength:0];
self.callback = callback;
self.bridge = bridgeRef;
self.expectedBytes = 0;
self.receivedBytes = 0;
self.options = options;
backgroundTask = [options valueForKey:@"IOSBackgroundTask"] == nil ? NO : [[options valueForKey:@"IOSBackgroundTask"] boolValue];
followRedirect = [options valueForKey:@"followRedirect"] == nil ? YES : [[options valueForKey:@"followRedirect"] boolValue];
isIncrement = [options valueForKey:@"increment"] == nil ? NO : [[options valueForKey:@"increment"] boolValue];
redirects = [[NSMutableArray alloc] init];
if(req.URL != nil)
[redirects addObject:req.URL.absoluteString];
// set response format
NSString * rnfbResp = [req.allHTTPHeaderFields valueForKey:@"RNFB-Response"];
if([[rnfbResp lowercaseString] isEqualToString:@"base64"])
responseFormat = BASE64;
else if([[rnfbResp lowercaseString] isEqualToString:@"utf8"])
responseFormat = UTF8;
else
responseFormat = AUTO;
NSString * path = [self.options valueForKey:CONFIG_FILE_PATH];
NSString * ext = [self.options valueForKey:CONFIG_FILE_EXT];
NSString * key = [self.options valueForKey:CONFIG_KEY];
__block NSURLSession * session;
bodyLength = contentLength;
// the session trust any SSL certification
NSURLSessionConfiguration *defaultConfigObject;
defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
if(backgroundTask)
{
defaultConfigObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:taskId];
}
// set request timeout
float timeout = [options valueForKey:@"timeout"] == nil ? -1 : [[options valueForKey:@"timeout"] floatValue];
if(timeout > 0)
{
defaultConfigObject.timeoutIntervalForRequest = timeout/1000;
}
defaultConfigObject.HTTPMaximumConnectionsPerHost = 10;
session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:taskQueue];
if(path != nil || [self.options valueForKey:CONFIG_USE_TEMP]!= nil)
{
respFile = YES;
NSString* cacheKey = taskId;
if (key != nil) {
cacheKey = [self md5:key];
if (cacheKey == nil) {
cacheKey = taskId;
}
destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]];
if ([[NSFileManager defaultManager] fileExistsAtPath:destPath]) {
callback(@[[NSNull null], RESP_TYPE_PATH, destPath]);
return;
}
}
if(path != nil)
destPath = path;
else
destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]];
}
else
{
respData = [[NSMutableData alloc] init];
respFile = NO;
}
__block NSURLSessionDataTask * task = [session dataTaskWithRequest:req];
[taskTable setObject:task forKey:taskId];
[task resume];
// network status indicator
if([[options objectForKey:CONFIG_INDICATOR] boolValue] == YES)
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
__block UIApplication * app = [UIApplication sharedApplication];
}
// #115 Invoke fetch.expire event on those expired requests so that the expired event can be handled
+ (void) emitExpiredTasks
{
NSEnumerator * emu = [expirationTable keyEnumerator];
NSString * key;
while((key = [emu nextObject]))
{
RCTBridge * bridge = [RNFetchBlob getRCTBridge];
NSData * args = @{ @"taskId": key };
[bridge.eventDispatcher sendDeviceEventWithName:EVENT_EXPIRE body:args];
}
// clear expired task entries
[expirationTable removeAllObjects];
expirationTable = [[NSMapTable alloc] init];
}
////////////////////////////////////////
//
// NSURLSession delegates
//
////////////////////////////////////////
#pragma mark NSURLSession delegate methods
#pragma mark - Received Response
// set expected content length on response received
- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
expectedBytes = [response expectedContentLength];
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
NSString * respType = @"";
respStatus = statusCode;
if ([response respondsToSelector:@selector(allHeaderFields)])
{
NSDictionary *headers = [httpResponse allHeaderFields];
NSString * respCType = [[RNFetchBlobReqBuilder getHeaderIgnoreCases:@"Content-Type" fromHeaders:headers] lowercaseString];
if(self.isServerPush == NO)
{
self.isServerPush = [[respCType lowercaseString] RNFBContainsString:@"multipart/x-mixed-replace;"];
}
if(self.isServerPush)
{
if(partBuffer != nil)
{
[self.bridge.eventDispatcher
sendDeviceEventWithName:EVENT_SERVER_PUSH
body:@{
@"taskId": taskId,
@"chunk": [partBuffer base64EncodedStringWithOptions:0],
}
];
}
partBuffer = [[NSMutableData alloc] init];
completionHandler(NSURLSessionResponseAllow);
return;
}
if(respCType != nil)
{
NSArray * extraBlobCTypes = [options objectForKey:CONFIG_EXTRA_BLOB_CTYPE];
if([respCType RNFBContainsString:@"text/"])
{
respType = @"text";
}
else if([respCType RNFBContainsString:@"application/json"])
{
respType = @"json";
}
// If extra blob content type is not empty, check if response type matches
else if( extraBlobCTypes != nil) {
for(NSString * substr in extraBlobCTypes)
{
if([respCType RNFBContainsString:[substr lowercaseString]])
{
respType = @"blob";
respFile = YES;
destPath = [RNFetchBlobFS getTempPath:taskId withExtension:nil];
break;
}
}
}
else
{
respType = @"blob";
// for XMLHttpRequest, switch response data handling strategy automatically
if([options valueForKey:@"auto"] == YES) {
respFile = YES;
destPath = [RNFetchBlobFS getTempPath:taskId withExtension:@""];
}
}
}
else
respType = @"text";
respInfo = @{
@"taskId": taskId,
@"state": @"2",
@"headers": headers,
@"redirects": redirects,
@"respType" : respType,
@"timeout" : @NO,
@"status": [NSNumber numberWithInteger:statusCode]
};
#pragma mark - handling cookies
// # 153 get cookies
if(response.URL != nil)
{
NSHTTPCookieStorage * cookieStore = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray<NSHTTPCookie *> * cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: headers forURL:response.URL];
if(cookies != nil && [cookies count] > 0) {
[cookieStore setCookies:cookies forURL:response.URL mainDocumentURL:nil];
}
}
[self.bridge.eventDispatcher
sendDeviceEventWithName: EVENT_STATE_CHANGE
body:respInfo
];
headers = nil;
respInfo = nil;
}
else
NSLog(@"oops");
if(respFile == YES)
{
@try{
NSFileManager * fm = [NSFileManager defaultManager];
NSString * folder = [destPath stringByDeletingLastPathComponent];
if(![fm fileExistsAtPath:folder])
{
[fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:nil];
}
BOOL overwrite = [options valueForKey:@"overwrite"] == nil ? YES : [[options valueForKey:@"overwrite"] boolValue];
BOOL appendToExistingFile = [destPath RNFBContainsString:@"?append=true"];
appendToExistingFile = !overwrite;
// For solving #141 append response data if the file already exists
// base on PR#139 @kejinliang
if(appendToExistingFile)
{
destPath = [destPath stringByReplacingOccurrencesOfString:@"?append=true" withString:@""];
}
if (![fm fileExistsAtPath:destPath])
{
[fm createFileAtPath:destPath contents:[[NSData alloc] init] attributes:nil];
}
writeStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:appendToExistingFile];
[writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[writeStream open];
}
@catch(NSException * ex)
{
NSLog(@"write file error");
}
}
completionHandler(NSURLSessionResponseAllow);
}
// download progress handler
- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
// For #143 handling multipart/x-mixed-replace response
if(self.isServerPush)
{
[partBuffer appendData:data];
return ;
}
NSNumber * received = [NSNumber numberWithLong:[data length]];
receivedBytes += [received longValue];
NSString * chunkString = @"";
if(isIncrement == YES)
{
chunkString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
if(respFile == NO)
{
[respData appendData:data];
}
else
{
[writeStream write:[data bytes] maxLength:[data length]];
}
RNFetchBlobProgress * pconfig = [progressTable valueForKey:taskId];
if(expectedBytes == 0)
return;
NSNumber * now =[NSNumber numberWithFloat:((float)receivedBytes/(float)expectedBytes)];
if(pconfig != nil && [pconfig shouldReport:now])
{
[self.bridge.eventDispatcher
sendDeviceEventWithName:EVENT_PROGRESS
body:@{
@"taskId": taskId,
@"written": [NSString stringWithFormat:@"%d", receivedBytes],
@"total": [NSString stringWithFormat:@"%d", expectedBytes],
@"chunk": chunkString
}
];
}
received = nil;
}
- (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error
{
if([session isEqual:session])
session = nil;
}
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
self.error = error;
NSString * errMsg = [NSNull null];
NSString * respStr = [NSNull null];
NSString * rnfbRespType = @"";
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
if(respInfo == nil)
{
respInfo = [NSNull null];
}
if(error != nil)
{
errMsg = [error localizedDescription];
}
if(respFile == YES)
{
[writeStream close];
rnfbRespType = RESP_TYPE_PATH;
respStr = destPath;
}
// base64 response
else {
// #73 fix unicode data encoding issue :
// when response type is BASE64, we should first try to encode the response data to UTF8 format
// if it turns out not to be `nil` that means the response data contains valid UTF8 string,
// in order to properly encode the UTF8 string, use URL encoding before BASE64 encoding.
NSString * utf8 = [[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding];
if(responseFormat == BASE64)
{
rnfbRespType = RESP_TYPE_BASE64;
respStr = [respData base64EncodedStringWithOptions:0];
}
else if (responseFormat == UTF8)
{
rnfbRespType = RESP_TYPE_UTF8;
respStr = utf8;
}
else
{
if(utf8 != nil)
{
rnfbRespType = RESP_TYPE_UTF8;
respStr = utf8;
}
else
{
rnfbRespType = RESP_TYPE_BASE64;
respStr = [respData base64EncodedStringWithOptions:0];
}
}
}
callback(@[ errMsg, rnfbRespType, respStr]);
@synchronized(taskTable, uploadProgressTable, progressTable)
{
if([taskTable objectForKey:taskId] == nil)
NSLog(@"object released by ARC.");
else
[taskTable removeObjectForKey:taskId];
[uploadProgressTable removeObjectForKey:taskId];
[progressTable removeObjectForKey:taskId];
}
respData = nil;
receivedBytes = 0;
[session finishTasksAndInvalidate];
}
// upload progress handler
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesWritten totalBytesExpectedToSend:(int64_t)totalBytesExpectedToWrite
{
RNFetchBlobProgress * pconfig = [uploadProgressTable valueForKey:taskId];
if(totalBytesExpectedToWrite == 0)
return;
NSNumber * now = [NSNumber numberWithFloat:((float)totalBytesWritten/(float)totalBytesExpectedToWrite)];
if(pconfig != nil && [pconfig shouldReport:now]) {
[self.bridge.eventDispatcher
sendDeviceEventWithName:EVENT_PROGRESS_UPLOAD
body:@{
@"taskId": taskId,
@"written": [NSString stringWithFormat:@"%d", totalBytesWritten],
@"total": [NSString stringWithFormat:@"%d", totalBytesExpectedToWrite]
}
];
}
}
+ (void) cancelRequest:(NSString *)taskId
{
NSURLSessionDataTask * task = [taskTable objectForKey:taskId];
if(task != nil && task.state == NSURLSessionTaskStateRunning)
[task cancel];
}
- (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable credantial))completionHandler
{
BOOL trusty = [options valueForKey:CONFIG_TRUSTY];
if(!trusty)
{
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
else
{
// completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
[self ZQURLSession:session didReceiveChallenge:challenge completionHandler:completionHandler];
}
}
- (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"sess done in background");
}
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler
{
if(followRedirect)
{
if(request.URL != nil)
[redirects addObject:[request.URL absoluteString]];
completionHandler(request);
}
else
{
completionHandler(nil);
}
}
- (void)ZQURLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
//挑战处理类型为 默认
/*
NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
NSURLSessionAuthChallengeUseCredential:使用指定的证书
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
*/
__weak typeof(self) weakSelf = self;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// sessionDidReceiveAuthenticationChallenge是自定义方法,用来如何应对服务器端的认证挑战
// 而这个证书就需要使用credentialForTrust:来创建一个NSURLCredential对象
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 创建挑战证书(注:挑战方式为UseCredential和PerformDefaultHandling都需要新建挑战证书)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 确定挑战的方式
if (credential) {
//证书挑战 设计policy,none,则跑到这里
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
// client authentication
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSFileManager *fileManager =[NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];
if ([[weakSelf class]extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
{
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void*certs[] = {certificate};
CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
credential =[NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
disposition =NSURLSessionAuthChallengeUseCredential;
}
}
}
//完成挑战
if (completionHandler) {
completionHandler(disposition, credential);
}
}
+(BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityError = errSecSuccess;
//client certificate password
NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"cplh123456"
forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge CFDictionaryRef)optionsDictionary,&items);
if(securityError == 0) {
CFDictionaryRef myIdentityAndTrust = (CFDictionaryRef)CFArrayGetValueAtIndex(items,0);
const void*tempIdentity =NULL;
tempIdentity= CFDictionaryGetValue (myIdentityAndTrust,kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void*tempTrust =NULL;
tempTrust = CFDictionaryGetValue(myIdentityAndTrust,kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failedwith error code %d",(int)securityError);
return NO;
}
return YES;
}
@end
注意: 在RN里使用fetch-blob时得配置trusty : true
三 : webview
D81CCB48-FEE5-48CE-8E4C-2031D01AC704.png9CA02E39-BCD3-4116-BA0A-F668777D59F0.png
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTWebView.h"
#import <UIKit/UIKit.h>
#import "RCTAutoInsetsProtocol.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "UIView+React.h"
NSString *const RCTJSNavigationScheme = @"react-js-navigation";
NSString *const RCTJSPostMessageHost = @"postMessage";
@interface RCTWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol,NSURLConnectionDataDelegate>
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
// https验证
@property (nonatomic,strong) NSURLConnection *urlConnection;
@property (nonatomic,strong) NSURLRequest *requestW;
@property (nonatomic) SSLAuthenticate authenticated;
@end
@implementation RCTWebView
{
UIWebView *_webView;
NSString *_injectedJavaScript;
}
- (void)dealloc
{
_webView.delegate = nil;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
super.backgroundColor = [UIColor clearColor];
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
_webView = [[UIWebView alloc] initWithFrame:self.bounds];
_webView.delegate = self;
[self addSubview:_webView];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)goForward
{
[_webView goForward];
}
- (void)goBack
{
[_webView goBack];
}
- (void)reload
{
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
if (request.URL && !_webView.request.URL.absoluteString.length) {
_requestW = request;
[_webView loadRequest:request];
}
else {
[_webView reload];
}
}
- (void)stopLoading
{
[_webView stopLoading];
}
- (void)postMessage:(NSString *)message
{
NSDictionary *eventInitDict = @{
@"data": message,
};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[_webView stringByEvaluatingJavaScriptFromString:source];
}
- (void)injectJavaScript:(NSString *)script
{
[_webView stringByEvaluatingJavaScriptFromString:script];
}
- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
_source = [source copy];
// Check for a static html source first
NSString *html = [RCTConvert NSString:source[@"html"]];
if (html) {
NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
if (!baseURL) {
baseURL = [NSURL URLWithString:@"about:blank"];
}
[_webView loadHTMLString:html baseURL:baseURL];
return;
}
NSURLRequest *request = [RCTConvert NSURLRequest:source];
_requestW = request;
// Because of the way React works, as pages redirect, we actually end up
// passing the redirect urls back here, so we ignore them if trying to load
// the same url. We'll expose a call to 'reload' to allow a user to load
// the existing page.
if ([request.URL isEqual:_webView.request.URL]) {
return;
}
if (!request.URL) {
// Clear the webview
[_webView loadHTMLString:@"" baseURL:nil];
return;
}
[_webView loadRequest:request];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
_webView.frame = self.bounds;
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
_contentInset = contentInset;
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:NO];
}
- (void)setScalesPageToFit:(BOOL)scalesPageToFit
{
if (_webView.scalesPageToFit != scalesPageToFit) {
_webView.scalesPageToFit = scalesPageToFit;
[_webView reload];
}
}
- (BOOL)scalesPageToFit
{
return _webView.scalesPageToFit;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
self.opaque = _webView.opaque = (alpha == 1.0);
_webView.backgroundColor = backgroundColor;
}
- (UIColor *)backgroundColor
{
return _webView.backgroundColor;
}
- (NSMutableDictionary<NSString *, id> *)baseEvent
{
NSMutableDictionary<NSString *, id> *event = [[NSMutableDictionary alloc] initWithDictionary:@{
@"url": _webView.request.URL.absoluteString ?: @"",
@"loading" : @(_webView.loading),
@"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
@"canGoBack": @(_webView.canGoBack),
@"canGoForward" : @(_webView.canGoForward),
}];
return event;
}
- (void)refreshContentInset
{
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:YES];
}
#pragma mark - UIWebViewDelegate methods
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
if ([request.URL.scheme rangeOfString:@"https"].location != NSNotFound) {
if (!_authenticated) {
// _authenticated = NO;
// __weak typeof(self) weakSelf = self;
//开启同步的请求去双向认证
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
[webView stopLoading];
return NO;
}else{
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
navigationTypes = @{
@(UIWebViewNavigationTypeLinkClicked): @"click",
@(UIWebViewNavigationTypeFormSubmitted): @"formsubmit",
@(UIWebViewNavigationTypeBackForward): @"backforward",
@(UIWebViewNavigationTypeReload): @"reload",
@(UIWebViewNavigationTypeFormResubmitted): @"formresubmit",
@(UIWebViewNavigationTypeOther): @"other",
};
});
// skip this for the JS Navigation handler
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
if (![self.delegate webView:self
shouldStartLoadForRequest:event
withCallback:_onShouldStartLoadWithRequest]) {
return NO;
}
}
if (_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
if (isTopFrame) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
_onLoadingStart(event);
}
}
if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) {
NSString *data = request.URL.query;
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"data": data,
}];
_onMessage(event);
}
// JS Navigation handler
return !isJSNavigation;
}
}else{
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
navigationTypes = @{
@(UIWebViewNavigationTypeLinkClicked): @"click",
@(UIWebViewNavigationTypeFormSubmitted): @"formsubmit",
@(UIWebViewNavigationTypeBackForward): @"backforward",
@(UIWebViewNavigationTypeReload): @"reload",
@(UIWebViewNavigationTypeFormResubmitted): @"formresubmit",
@(UIWebViewNavigationTypeOther): @"other",
};
});
// skip this for the JS Navigation handler
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
if (![self.delegate webView:self
shouldStartLoadForRequest:event
withCallback:_onShouldStartLoadWithRequest]) {
return NO;
}
}
if (_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
if (isTopFrame) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
_onLoadingStart(event);
}
}
if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) {
NSString *data = request.URL.query;
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"data": data,
}];
_onMessage(event);
}
// JS Navigation handler
return !isJSNavigation;
}
}
- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if (_onLoadingError) {
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
// a new URL in the WebView before the previous one came back. We can just
// ignore these since they aren't real errors.
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
return;
}
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"domain": error.domain,
@"code": @(error.code),
@"description": error.localizedDescription,
}];
_onLoadingError(event);
}
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
if (_messagingEnabled) {
#if RCT_DEV
// See isNative in lodash
NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
BOOL postMessageIsNative = [
[webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
isEqualToString:@"true"
];
if (!postMessageIsNative) {
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
#endif
NSString *source = [NSString stringWithFormat:
@"window.originalPostMessage = window.postMessage;"
"window.postMessage = function(data) {"
"window.location = '%@://%@?' + encodeURIComponent(String(data));"
"};", RCTJSNavigationScheme, RCTJSPostMessageHost
];
[webView stringByEvaluatingJavaScriptFromString:source];
}
if (_injectedJavaScript != nil) {
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
event[@"jsEvaluationValue"] = jsEvaluationValue;
_onLoadingFinish(event);
}
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) {
_onLoadingFinish([self baseEvent]);
}
}
#pragma mark ***NSURLConnection代理方法***
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSURLCredential * credential;
assert(challenge != nil);
credential = nil;
NSLog(@"----收到质询----");
NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod];
if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSLog(@"----服务器验证客户端----");
NSString *host = challenge.protectionSpace.host;
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
NSMutableArray *polices = [NSMutableArray array];
if (NO) {
[polices addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)host)];
}else{
[polices addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)polices);
//导入证书
NSString *path = [[NSBundle mainBundle] pathForResource:@"CNPCCA" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:path];
NSMutableArray *pinnedCerts = [NSMutableArray arrayWithObjects:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData), nil];
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCerts);
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
NSLog(@"----客户端验证服务端----");
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:p12]) {
NSLog(@"客户端.p12证书 不存在!");
}else{
NSData *pkcs12Data = [NSData dataWithContentsOfFile:p12];
if ([self extractIdentity:&identity andTrust:&trust fromPKCS12Data:pkcs12Data]) {
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void *certs[] = {certificate};
CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistencePermanent];
}
}
}
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
- (BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityErr = errSecSuccess;
//输入客户端证书密码
NSDictionary *optionsDic = [NSDictionary dictionaryWithObject:@"cplh123456" forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityErr = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDic, &items);
if (securityErr == errSecSuccess) {
CFDictionaryRef mineIdentAndTrust = CFArrayGetValueAtIndex(items, 0);
const void *tmpIdentity = NULL;
tmpIdentity = CFDictionaryGetValue(mineIdentAndTrust, kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tmpIdentity;
const void *tmpTrust = NULL;
tmpTrust = CFDictionaryGetValue(mineIdentAndTrust, kSecImportItemTrust);
*outTrust = (SecTrustRef)tmpTrust;
}else{
return false;
}
return true;
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse {
_authenticated = YES;
//webview 重新加载请求。
//
// NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//
// [[session dataTaskWithRequest:_requestW completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//
// NSLog(@"%s",__FUNCTION__);
// NSLog(@"RESPONSE:%@",response);
// NSLog(@"ERROR:%@",error);
//
// NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// NSLog(@"dataString:%@",dataString);
//
// [self loadHTMLString:dataString baseURL:nil];
// }] resume];
[_webView loadRequest:_requestW];
[connection cancel];
}
@end
网友评论