diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9fc19c --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/screenshots + +#Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ \ No newline at end of file diff --git a/AMapFoundationKit.framework/AMapFoundationKit b/AMapFoundationKit.framework/AMapFoundationKit new file mode 100755 index 0000000..6734ca5 Binary files /dev/null and b/AMapFoundationKit.framework/AMapFoundationKit differ diff --git a/AMapFoundationKit.framework/Headers/AMapFoundationKit.h b/AMapFoundationKit.framework/Headers/AMapFoundationKit.h new file mode 100755 index 0000000..fdeeef1 --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapFoundationKit.h @@ -0,0 +1,15 @@ +// +// AMapFoundationKit.h +// AMapFoundationKit +// +// Created by xiaoming han on 15/10/28. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import +#import +#import +#import + +#import diff --git a/AMapFoundationKit.framework/Headers/AMapFoundationVersion.h b/AMapFoundationKit.framework/Headers/AMapFoundationVersion.h new file mode 100755 index 0000000..bb343d9 --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapFoundationVersion.h @@ -0,0 +1,19 @@ +// +// AMapFoundationVersion.h +// AMapFoundation +// +// Created by xiaoming han on 15/10/26. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import + +#ifndef AMapFoundationVersion_h +#define AMapFoundationVersion_h + +#define AMapFoundationVersionNumber 10403 + +FOUNDATION_EXTERN NSString * const AMapFoundationVersion; +FOUNDATION_EXTERN NSString * const AMapFoundationName; + +#endif /* AMapFoundationVersion_h */ diff --git a/AMapFoundationKit.framework/Headers/AMapServices.h b/AMapFoundationKit.framework/Headers/AMapServices.h new file mode 100755 index 0000000..1c0d7b4 --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapServices.h @@ -0,0 +1,28 @@ +// +// AMapSearchServices.h +// AMapSearchKit +// +// Created by xiaoming han on 15/6/18. +// Copyright (c) 2015年 xiaoming han. All rights reserved. +// + +#import + +///高德SDK服务类 +@interface AMapServices : NSObject + +/** + * @brief 获取单例 + */ ++ (AMapServices *)sharedServices; + +///APIkey。设置key,需要绑定对应的bundle id。 +@property (nonatomic, copy) NSString *apiKey; + +///是否开启HTTPS,从1.3.3版本开始默认为YES。 +@property (nonatomic, assign) BOOL enableHTTPS; + +///是否启用崩溃日志上传。默认为YES, 只有在真机上设置有效。\n开启崩溃日志上传有助于我们更好的了解SDK的状况,可以帮助我们持续优化和改进SDK。需要注意的是,SDK内部是通过设置NSUncaughtExceptionHandler来捕获异常的,如果您的APP中使用了其他收集崩溃日志的SDK,或者自己有设置NSUncaughtExceptionHandler的话,请保证 AMapServices 的初始化是在其他设置NSUncaughtExceptionHandler操作之后进行的,我们的handler会再处理完异常后调用前一次设置的handler,保证之前设置的handler会被执行。 +@property (nonatomic, assign) BOOL crashReportEnabled; + +@end diff --git a/AMapFoundationKit.framework/Headers/AMapURLSearch.h b/AMapFoundationKit.framework/Headers/AMapURLSearch.h new file mode 100755 index 0000000..9379f4f --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapURLSearch.h @@ -0,0 +1,41 @@ +// +// AMapURLSearch.h +// AMapFoundation +// +// Created by xiaoming han on 15/10/28. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import "AMapURLSearchConfig.h" + +///调起高德地图URL进行搜索,若是调起失败,可使用`+ (void)getLatestAMapApp;`方法获取最新版高德地图app. +@interface AMapURLSearch : NSObject + +/** + * @brief 打开高德地图AppStore页面 + */ ++ (void)getLatestAMapApp; + +/** + * @brief 调起高德地图app驾车导航. + * @param config 配置参数. + * @return 是否成功.若为YES则成功调起,若为NO则无法调起. + */ ++ (BOOL)openAMapNavigation:(AMapNaviConfig *)config; + +/** + * @brief 调起高德地图app进行路径规划. + * @param config 配置参数. + * @return 是否成功. + */ ++ (BOOL)openAMapRouteSearch:(AMapRouteConfig *)config; + +/** + * @brief 调起高德地图app进行POI搜索. + * @param config 配置参数. + * @return 是否成功. + */ ++ (BOOL)openAMapPOISearch:(AMapPOIConfig *)config; + +@end diff --git a/AMapFoundationKit.framework/Headers/AMapURLSearchConfig.h b/AMapFoundationKit.framework/Headers/AMapURLSearchConfig.h new file mode 100755 index 0000000..666b492 --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapURLSearchConfig.h @@ -0,0 +1,79 @@ +// +// MAMapURLSearchConfig.h +// MAMapKitNew +// +// Created by xiaoming han on 15/5/25. +// Copyright (c) 2015年 xiaoming han. All rights reserved. +// + +#import +#import +#import "AMapURLSearchType.h" + +///导航配置信息 +@interface AMapNaviConfig : NSObject + +///应用返回的Scheme +@property (nonatomic, copy) NSString *appScheme; + +///应用名称 +@property (nonatomic, copy) NSString *appName; + +///终点 +@property (nonatomic, assign) CLLocationCoordinate2D destination; + +///导航策略 +@property (nonatomic, assign) AMapDrivingStrategy strategy; + +@end + +#pragma mark - + +///路径搜索配置信息 +@interface AMapRouteConfig : NSObject + +///应用返回的Scheme +@property (nonatomic, copy) NSString *appScheme; + +///应用名称 +@property (nonatomic, copy) NSString *appName; + +///起点坐标 +@property (nonatomic, assign) CLLocationCoordinate2D startCoordinate; + +///终点坐标 +@property (nonatomic, assign) CLLocationCoordinate2D destinationCoordinate; + +///驾车策略 +@property (nonatomic, assign) AMapDrivingStrategy drivingStrategy; + +///公交策略 +@property (nonatomic, assign) AMapTransitStrategy transitStrategy; + +///路径规划类型 +@property (nonatomic, assign) AMapRouteSearchType routeType; + +@end + +#pragma mark - + +///POI搜索配置信息 +@interface AMapPOIConfig : NSObject + +///应用返回的Scheme +@property (nonatomic, copy) NSString *appScheme; + +///应用名称 +@property (nonatomic, copy) NSString *appName; + +///搜索关键字 +@property (nonatomic, copy) NSString *keywords; + +///左上角坐标 +@property (nonatomic, assign) CLLocationCoordinate2D leftTopCoordinate; + +///右下角坐标 +@property (nonatomic, assign) CLLocationCoordinate2D rightBottomCoordinate; + +@end + diff --git a/AMapFoundationKit.framework/Headers/AMapURLSearchType.h b/AMapFoundationKit.framework/Headers/AMapURLSearchType.h new file mode 100755 index 0000000..424d905 --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapURLSearchType.h @@ -0,0 +1,44 @@ +// +// MAMapURLSearchType.h +// MAMapKitNew +// +// Created by xiaoming han on 15/5/25. +// Copyright (c) 2015年 xiaoming han. All rights reserved. +// + +///驾车策略 +typedef NS_ENUM(NSInteger, AMapDrivingStrategy) +{ + AMapDrivingStrategyFastest = 0, ///<速度最快 + AMapDrivingStrategyMinFare = 1, ///<避免收费 + AMapDrivingStrategyShortest = 2, ///<距离最短 + + AMapDrivingStrategyNoHighways = 3, ///<不走高速 + AMapDrivingStrategyAvoidCongestion = 4, ///<躲避拥堵 + + AMapDrivingStrategyAvoidHighwaysAndFare = 5, ///<不走高速且避免收费 + AMapDrivingStrategyAvoidHighwaysAndCongestion = 6, ///<不走高速且躲避拥堵 + AMapDrivingStrategyAvoidFareAndCongestion = 7, ///<躲避收费和拥堵 + AMapDrivingStrategyAvoidHighwaysAndFareAndCongestion = 8 ///<不走高速躲避收费和拥堵 +}; + +///公交策略 +typedef NS_ENUM(NSInteger, AMapTransitStrategy) +{ + AMapTransitStrategyFastest = 0,///<最快捷 + AMapTransitStrategyMinFare = 1,///<最经济 + AMapTransitStrategyMinTransfer = 2,///<最少换乘 + AMapTransitStrategyMinWalk = 3,///<最少步行 + AMapTransitStrategyMostComfortable = 4,///<最舒适 + AMapTransitStrategyAvoidSubway = 5,///<不乘地铁 +}; + +///路径规划类型 +typedef NS_ENUM(NSInteger, AMapRouteSearchType) +{ + AMapRouteSearchTypeDriving = 0, ///<驾车 + AMapRouteSearchTypeTransit = 1, ///<公交 + AMapRouteSearchTypeWalking = 2, ///<步行 +}; + + diff --git a/AMapFoundationKit.framework/Headers/AMapUtility.h b/AMapFoundationKit.framework/Headers/AMapUtility.h new file mode 100755 index 0000000..e5e5d87 --- /dev/null +++ b/AMapFoundationKit.framework/Headers/AMapUtility.h @@ -0,0 +1,49 @@ +// +// AMapUtility.h +// AMapFoundation +// +// Created by xiaoming han on 15/10/27. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import + +//工具方法 + +/** + * @brief 如果字符串为nil则返回空字符串 + */ +FOUNDATION_STATIC_INLINE NSString * AMapEmptyStringIfNil(NSString *s) +{ + return s ? s : @""; +} + +///坐标类型枚举 +typedef NS_ENUM(NSUInteger, AMapCoordinateType) +{ + AMapCoordinateTypeBaidu = 0, /// + +@implementation NSArray (CDVJSONSerializingPrivate) + +- (NSString*)cdv_JSONString +{ + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:self + options:0 + error:&error]; + + if (error != nil) { + NSLog(@"NSArray JSONString error: %@", [error localizedDescription]); + return nil; + } else { + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } +} + +@end + +@implementation NSDictionary (CDVJSONSerializingPrivate) + +- (NSString*)cdv_JSONString +{ + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:self + options:NSJSONWritingPrettyPrinted + error:&error]; + + if (error != nil) { + NSLog(@"NSDictionary JSONString error: %@", [error localizedDescription]); + return nil; + } else { + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } +} + +@end + +@implementation NSString (CDVJSONSerializingPrivate) + +- (id)cdv_JSONObject +{ + NSError* error = nil; + id object = [NSJSONSerialization JSONObjectWithData:[self dataUsingEncoding:NSUTF8StringEncoding] + options:NSJSONReadingMutableContainers + error:&error]; + + if (error != nil) { + NSLog(@"NSString JSONObject error: %@", [error localizedDescription]); + } + + return object; +} + +- (id)cdv_JSONFragment +{ + NSError* error = nil; + id object = [NSJSONSerialization JSONObjectWithData:[self dataUsingEncoding:NSUTF8StringEncoding] + options:NSJSONReadingAllowFragments + error:&error]; + + if (error != nil) { + NSLog(@"NSString JSONObject error: %@", [error localizedDescription]); + } + + return object; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/CDVPlugin+Private.h b/msext.xcodeproj/CordovaLib/Classes/Private/CDVPlugin+Private.h new file mode 100755 index 0000000..f88638c --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/CDVPlugin+Private.h @@ -0,0 +1,24 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +@interface CDVPlugin (Private) + +- (instancetype)initWithWebViewEngine:(id )theWebViewEngine; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVGestureHandler/CDVGestureHandler.h b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVGestureHandler/CDVGestureHandler.h new file mode 100755 index 0000000..510b6eb --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVGestureHandler/CDVGestureHandler.h @@ -0,0 +1,26 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" + +@interface CDVGestureHandler : CDVPlugin + +@property (nonatomic, strong) UILongPressGestureRecognizer* lpgr; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVGestureHandler/CDVGestureHandler.m b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVGestureHandler/CDVGestureHandler.m new file mode 100755 index 0000000..242ac55 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVGestureHandler/CDVGestureHandler.m @@ -0,0 +1,74 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVGestureHandler.h" + +@implementation CDVGestureHandler + +- (void)pluginInitialize +{ + [self applyLongPressFix]; +} + +- (void)applyLongPressFix +{ + // You can't suppress 3D Touch and still have regular longpress, + // so if this is false, let's not consider the 3D Touch setting at all. + if (![self.commandDelegate.settings objectForKey:@"suppresseslongpressgesture"] || + ![[self.commandDelegate.settings objectForKey:@"suppresseslongpressgesture"] boolValue]) { + return; + } + + self.lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGestures:)]; + self.lpgr.minimumPressDuration = 0.45f; + self.lpgr.allowableMovement = 100.0f; + + // 0.45 is ok for 'regular longpress', 0.05-0.08 is required for '3D Touch longpress', + // but since this will also kill onclick handlers (not ontouchend) it's optional. + if ([self.commandDelegate.settings objectForKey:@"suppresses3dtouchgesture"] && + [[self.commandDelegate.settings objectForKey:@"suppresses3dtouchgesture"] boolValue]) { + self.lpgr.minimumPressDuration = 0.05f; + } + + NSArray *views = self.webView.subviews; + if (views.count == 0) { + NSLog(@"No webview subviews found, not applying the longpress fix."); + return; + } + for (int i=0; i + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVIntentAndNavigationFilter/CDVIntentAndNavigationFilter.m b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVIntentAndNavigationFilter/CDVIntentAndNavigationFilter.m new file mode 100755 index 0000000..365db3f --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVIntentAndNavigationFilter/CDVIntentAndNavigationFilter.m @@ -0,0 +1,112 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVIntentAndNavigationFilter.h" +#import + +@interface CDVIntentAndNavigationFilter () + +@property (nonatomic, readwrite) NSMutableArray* allowIntents; +@property (nonatomic, readwrite) NSMutableArray* allowNavigations; +@property (nonatomic, readwrite) CDVWhitelist* allowIntentsWhitelist; +@property (nonatomic, readwrite) CDVWhitelist* allowNavigationsWhitelist; + +@end + +@implementation CDVIntentAndNavigationFilter + +#pragma mark NSXMLParserDelegate + +- (void)parser:(NSXMLParser*)parser didStartElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName attributes:(NSDictionary*)attributeDict +{ + if ([elementName isEqualToString:@"allow-navigation"]) { + [self.allowNavigations addObject:attributeDict[@"href"]]; + } + if ([elementName isEqualToString:@"allow-intent"]) { + [self.allowIntents addObject:attributeDict[@"href"]]; + } +} + +- (void)parserDidStartDocument:(NSXMLParser*)parser +{ + // file: url are added by default + self.allowNavigations = [[NSMutableArray alloc] initWithArray:@[ @"file://" ]]; + // no intents are added by default + self.allowIntents = [[NSMutableArray alloc] init]; +} + +- (void)parserDidEndDocument:(NSXMLParser*)parser +{ + self.allowIntentsWhitelist = [[CDVWhitelist alloc] initWithArray:self.allowIntents]; + self.allowNavigationsWhitelist = [[CDVWhitelist alloc] initWithArray:self.allowNavigations]; +} + +- (void)parser:(NSXMLParser*)parser parseErrorOccurred:(NSError*)parseError +{ + NSAssert(NO, @"config.xml parse error line %ld col %ld", (long)[parser lineNumber], (long)[parser columnNumber]); +} + +#pragma mark CDVPlugin + +- (void)pluginInitialize +{ + if ([self.viewController isKindOfClass:[CDVViewController class]]) { + [(CDVViewController*)self.viewController parseSettingsWithParser:self]; + } +} + +- (BOOL)shouldOverrideLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + NSString* allowIntents_whitelistRejectionFormatString = @"ERROR External navigation rejected - not set for url='%@'"; + NSString* allowNavigations_whitelistRejectionFormatString = @"ERROR Internal navigation rejected - not set for url='%@'"; + + NSURL* url = [request URL]; + BOOL allowNavigationsPass = NO; + NSMutableArray* errorLogs = [NSMutableArray array]; + + switch (navigationType) { + case UIWebViewNavigationTypeLinkClicked: + // Note that the rejection strings will *only* print if + // it's a link click (and url is not whitelisted by ) + if ([self.allowIntentsWhitelist URLIsAllowed:url logFailure:NO]) { + // the url *is* in a tag, push to the system + [[UIApplication sharedApplication] openURL:url]; + return NO; + } else { + [errorLogs addObject:[NSString stringWithFormat:allowIntents_whitelistRejectionFormatString, [url absoluteString]]]; + } + // fall through, to check whether you can load this in the webview + default: + // check whether we can internally navigate to this url + allowNavigationsPass = [self.allowNavigationsWhitelist URLIsAllowed:url logFailure:NO]; + // log all failures only when this last filter fails + if (!allowNavigationsPass){ + [errorLogs addObject:[NSString stringWithFormat:allowNavigations_whitelistRejectionFormatString, [url absoluteString]]]; + + // this is the last filter and it failed, now print out all previous error logs + for (NSString* errorLog in errorLogs) { + NSLog(@"%@", errorLog); + } + } + + return allowNavigationsPass; + } +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.h b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.h new file mode 100755 index 0000000..dec6ab3 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.h @@ -0,0 +1,50 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" + +#define kCDVLocalStorageErrorDomain @"kCDVLocalStorageErrorDomain" +#define kCDVLocalStorageFileOperationError 1 + +@interface CDVLocalStorage : CDVPlugin + +@property (nonatomic, readonly, strong) NSMutableArray* backupInfo; + +- (BOOL)shouldBackup; +- (BOOL)shouldRestore; +- (void)backup:(CDVInvokedUrlCommand*)command; +- (void)restore:(CDVInvokedUrlCommand*)command; + ++ (void)__fixupDatabaseLocationsWithBackupType:(NSString*)backupType; +// Visible for testing. ++ (BOOL)__verifyAndFixDatabaseLocationsWithAppPlistDict:(NSMutableDictionary*)appPlistDict + bundlePath:(NSString*)bundlePath + fileManager:(NSFileManager*)fileManager; +@end + +@interface CDVBackupInfo : NSObject + +@property (nonatomic, copy) NSString* original; +@property (nonatomic, copy) NSString* backup; +@property (nonatomic, copy) NSString* label; + +- (BOOL)shouldBackup; +- (BOOL)shouldRestore; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.m b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.m new file mode 100755 index 0000000..252dfaf --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.m @@ -0,0 +1,487 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVLocalStorage.h" +#import "CDV.h" + +@interface CDVLocalStorage () + +@property (nonatomic, readwrite, strong) NSMutableArray* backupInfo; // array of CDVBackupInfo objects +@property (nonatomic, readwrite, weak) id webviewDelegate; + +@end + +@implementation CDVLocalStorage + +@synthesize backupInfo, webviewDelegate; + +- (void)pluginInitialize +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResignActive) + name:UIApplicationWillResignActiveNotification object:nil]; + BOOL cloudBackup = [@"cloud" isEqualToString : self.commandDelegate.settings[[@"BackupWebStorage" lowercaseString]]]; + + self.backupInfo = [[self class] createBackupInfoWithCloudBackup:cloudBackup]; +} + +#pragma mark - +#pragma mark Plugin interface methods + ++ (NSMutableArray*)createBackupInfoWithTargetDir:(NSString*)targetDir backupDir:(NSString*)backupDir targetDirNests:(BOOL)targetDirNests backupDirNests:(BOOL)backupDirNests rename:(BOOL)rename +{ + /* + This "helper" does so much work and has so many options it would probably be clearer to refactor the whole thing. + Basically, there are three database locations: + + 1. "Normal" dir -- LIB// + 2. "Caches" dir -- LIB/Caches/ + 3. "Backup" dir -- DOC/Backups/ + + And between these three, there are various migration paths, most of which only consider 2 of the 3, which is why this helper is based on 2 locations and has a notion of "direction". + */ + NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:3]; + + NSString* original; + NSString* backup; + CDVBackupInfo* backupItem; + + // ////////// LOCALSTORAGE + + original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/file__0.localstorage":@"file__0.localstorage"]; + backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")]; + backup = [backup stringByAppendingPathComponent:(rename ? @"localstorage.appdata.db" : @"file__0.localstorage")]; + + backupItem = [[CDVBackupInfo alloc] init]; + backupItem.backup = backup; + backupItem.original = original; + backupItem.label = @"localStorage database"; + + [backupInfo addObject:backupItem]; + + // ////////// WEBSQL MAIN DB + + original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/Databases.db":@"Databases.db"]; + backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")]; + backup = [backup stringByAppendingPathComponent:(rename ? @"websqlmain.appdata.db" : @"Databases.db")]; + + backupItem = [[CDVBackupInfo alloc] init]; + backupItem.backup = backup; + backupItem.original = original; + backupItem.label = @"websql main database"; + + [backupInfo addObject:backupItem]; + + // ////////// WEBSQL DATABASES + + original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/file__0":@"file__0"]; + backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")]; + backup = [backup stringByAppendingPathComponent:(rename ? @"websqldbs.appdata.db" : @"file__0")]; + + backupItem = [[CDVBackupInfo alloc] init]; + backupItem.backup = backup; + backupItem.original = original; + backupItem.label = @"websql databases"; + + [backupInfo addObject:backupItem]; + + return backupInfo; +} + ++ (NSMutableArray*)createBackupInfoWithCloudBackup:(BOOL)cloudBackup +{ + // create backup info from backup folder to caches folder + NSString* appLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* appDocumentsFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* cacheFolder = [appLibraryFolder stringByAppendingPathComponent:@"Caches"]; + NSString* backupsFolder = [appDocumentsFolder stringByAppendingPathComponent:@"Backups"]; + + // create the backups folder, if needed + [[NSFileManager defaultManager] createDirectoryAtPath:backupsFolder withIntermediateDirectories:YES attributes:nil error:nil]; + + [self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:backupsFolder] skip:!cloudBackup]; + + return [self createBackupInfoWithTargetDir:cacheFolder backupDir:backupsFolder targetDirNests:NO backupDirNests:NO rename:YES]; +} + ++ (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL*)URL skip:(BOOL)skip +{ + NSError* error = nil; + BOOL success = [URL setResourceValue:[NSNumber numberWithBool:skip] forKey:NSURLIsExcludedFromBackupKey error:&error]; + + if (!success) { + NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error); + } + return success; +} + ++ (BOOL)copyFrom:(NSString*)src to:(NSString*)dest error:(NSError* __autoreleasing*)error +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + + if (![fileManager fileExistsAtPath:src]) { + NSString* errorString = [NSString stringWithFormat:@"%@ file does not exist.", src]; + if (error != NULL) { + (*error) = [NSError errorWithDomain:kCDVLocalStorageErrorDomain + code:kCDVLocalStorageFileOperationError + userInfo:[NSDictionary dictionaryWithObject:errorString + forKey:NSLocalizedDescriptionKey]]; + } + return NO; + } + + // generate unique filepath in temp directory + CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); + CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + NSString* tempBackup = [[NSTemporaryDirectory() stringByAppendingPathComponent:(__bridge NSString*)uuidString] stringByAppendingPathExtension:@"bak"]; + CFRelease(uuidString); + CFRelease(uuidRef); + + BOOL destExists = [fileManager fileExistsAtPath:dest]; + + // backup the dest + if (destExists && ![fileManager copyItemAtPath:dest toPath:tempBackup error:error]) { + return NO; + } + + // remove the dest + if (destExists && ![fileManager removeItemAtPath:dest error:error]) { + return NO; + } + + // create path to dest + if (!destExists && ![fileManager createDirectoryAtPath:[dest stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:error]) { + return NO; + } + + // copy src to dest + if ([fileManager copyItemAtPath:src toPath:dest error:error]) { + // success - cleanup - delete the backup to the dest + if ([fileManager fileExistsAtPath:tempBackup]) { + [fileManager removeItemAtPath:tempBackup error:error]; + } + return YES; + } else { + // failure - we restore the temp backup file to dest + [fileManager copyItemAtPath:tempBackup toPath:dest error:error]; + // cleanup - delete the backup to the dest + if ([fileManager fileExistsAtPath:tempBackup]) { + [fileManager removeItemAtPath:tempBackup error:error]; + } + return NO; + } +} + +- (BOOL)shouldBackup +{ + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldBackup]) { + return YES; + } + } + + return NO; +} + +- (BOOL)shouldRestore +{ + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldRestore]) { + return YES; + } + } + + return NO; +} + +/* copy from webkitDbLocation to persistentDbLocation */ +- (void)backup:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + + NSError* __autoreleasing error = nil; + CDVPluginResult* result = nil; + NSString* message = nil; + + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldBackup]) { + [[self class] copyFrom:info.original to:info.backup error:&error]; + + if (callbackId) { + if (error == nil) { + message = [NSString stringWithFormat:@"Backed up: %@", info.label]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + message = [NSString stringWithFormat:@"Error in CDVLocalStorage (%@) backup: %@", info.label, [error localizedDescription]]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + } + } + } +} + +/* copy from persistentDbLocation to webkitDbLocation */ +- (void)restore:(CDVInvokedUrlCommand*)command +{ + NSError* __autoreleasing error = nil; + CDVPluginResult* result = nil; + NSString* message = nil; + + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldRestore]) { + [[self class] copyFrom:info.backup to:info.original error:&error]; + + if (error == nil) { + message = [NSString stringWithFormat:@"Restored: %@", info.label]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + message = [NSString stringWithFormat:@"Error in CDVLocalStorage (%@) restore: %@", info.label, [error localizedDescription]]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + } + } +} + ++ (void)__fixupDatabaseLocationsWithBackupType:(NSString*)backupType +{ + [self __verifyAndFixDatabaseLocations]; + [self __restoreLegacyDatabaseLocationsWithBackupType:backupType]; +} + ++ (void)__verifyAndFixDatabaseLocations +{ + NSBundle* mainBundle = [NSBundle mainBundle]; + NSString* bundlePath = [[mainBundle bundlePath] stringByDeletingLastPathComponent]; + NSString* bundleIdentifier = [[mainBundle infoDictionary] objectForKey:@"CFBundleIdentifier"]; + NSString* appPlistPath = [bundlePath stringByAppendingPathComponent:[NSString stringWithFormat:@"Library/Preferences/%@.plist", bundleIdentifier]]; + + NSMutableDictionary* appPlistDict = [NSMutableDictionary dictionaryWithContentsOfFile:appPlistPath]; + BOOL modified = [[self class] __verifyAndFixDatabaseLocationsWithAppPlistDict:appPlistDict + bundlePath:bundlePath + fileManager:[NSFileManager defaultManager]]; + + if (modified) { + BOOL ok = [appPlistDict writeToFile:appPlistPath atomically:YES]; + [[NSUserDefaults standardUserDefaults] synchronize]; + NSLog(@"Fix applied for database locations?: %@", ok ? @"YES" : @"NO"); + } +} + ++ (BOOL)__verifyAndFixDatabaseLocationsWithAppPlistDict:(NSMutableDictionary*)appPlistDict + bundlePath:(NSString*)bundlePath + fileManager:(NSFileManager*)fileManager +{ + NSString* libraryCaches = @"Library/Caches"; + NSString* libraryWebKit = @"Library/WebKit"; + + NSArray* keysToCheck = [NSArray arrayWithObjects: + @"WebKitLocalStorageDatabasePathPreferenceKey", + @"WebDatabaseDirectory", + nil]; + + BOOL dirty = NO; + + for (NSString* key in keysToCheck) { + NSString* value = [appPlistDict objectForKey:key]; + // verify key exists, and path is in app bundle, if not - fix + if ((value != nil) && ![value hasPrefix:bundlePath]) { + // the pathSuffix to use may be wrong - OTA upgrades from < 5.1 to 5.1 do keep the old path Library/WebKit, + // while Xcode synced ones do change the storage location to Library/Caches + NSString* newBundlePath = [bundlePath stringByAppendingPathComponent:libraryCaches]; + if (![fileManager fileExistsAtPath:newBundlePath]) { + newBundlePath = [bundlePath stringByAppendingPathComponent:libraryWebKit]; + } + [appPlistDict setValue:newBundlePath forKey:key]; + dirty = YES; + } + } + + return dirty; +} + ++ (void)__restoreLegacyDatabaseLocationsWithBackupType:(NSString*)backupType +{ + // on iOS 6, if you toggle between cloud/local backup, you must move database locations. Default upgrade from iOS5.1 to iOS6 is like a toggle from local to cloud. + NSString* appLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* appDocumentsFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + + NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:0]; + + if ([backupType isEqualToString:@"cloud"]) { +#ifdef DEBUG + NSLog(@"\n\nStarted backup to iCloud! Please be careful." + "\nYour application might be rejected by Apple if you store too much data." + "\nFor more information please read \"iOS Data Storage Guidelines\" at:" + "\nhttps://developer.apple.com/icloud/documentation/data-storage/" + "\nTo disable web storage backup to iCloud, set the BackupWebStorage preference to \"local\" in the Cordova config.xml file\n\n"); +#endif + // We would like to restore old backups/caches databases to the new destination (nested in lib folder) + [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appDocumentsFolder stringByAppendingPathComponent:@"Backups"] targetDirNests:YES backupDirNests:NO rename:YES]]; + [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] targetDirNests:YES backupDirNests:NO rename:NO]]; + } else { + // For ios6 local backups we also want to restore from Backups dir -- but we don't need to do that here, since the plugin will do that itself. + [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] backupDir:appLibraryFolder targetDirNests:NO backupDirNests:YES rename:NO]]; + } + + NSFileManager* manager = [NSFileManager defaultManager]; + + for (CDVBackupInfo* info in backupInfo) { + if ([manager fileExistsAtPath:info.backup]) { + if ([info shouldRestore]) { + NSLog(@"Restoring old webstorage backup. From: '%@' To: '%@'.", info.backup, info.original); + [self copyFrom:info.backup to:info.original error:nil]; + } + NSLog(@"Removing old webstorage backup: '%@'.", info.backup); + [manager removeItemAtPath:info.backup error:nil]; + } + } + + [[NSUserDefaults standardUserDefaults] setBool:[backupType isEqualToString:@"cloud"] forKey:@"WebKitStoreWebDataForBackup"]; +} + +#pragma mark - +#pragma mark Notification handlers + +- (void)onResignActive +{ + UIDevice* device = [UIDevice currentDevice]; + NSNumber* exitsOnSuspend = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationExitsOnSuspend"]; + + BOOL isMultitaskingSupported = [device respondsToSelector:@selector(isMultitaskingSupported)] && [device isMultitaskingSupported]; + + if (exitsOnSuspend == nil) { // if it's missing, it should be NO (i.e. multi-tasking on by default) + exitsOnSuspend = [NSNumber numberWithBool:NO]; + } + + if (exitsOnSuspend) { + [self backup:nil]; + } else if (isMultitaskingSupported) { + __block UIBackgroundTaskIdentifier backgroundTaskID = UIBackgroundTaskInvalid; + + backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID]; + backgroundTaskID = UIBackgroundTaskInvalid; + NSLog(@"Background task to backup WebSQL/LocalStorage expired."); + }]; + CDVLocalStorage __weak* weakSelf = self; + [self.commandDelegate runInBackground:^{ + [weakSelf backup:nil]; + + [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID]; + backgroundTaskID = UIBackgroundTaskInvalid; + }]; + } +} + +- (void)onAppTerminate +{ + [self onResignActive]; +} + +- (void)onReset +{ + [self restore:nil]; +} + +@end + +#pragma mark - +#pragma mark CDVBackupInfo implementation + +@implementation CDVBackupInfo + +@synthesize original, backup, label; + +- (BOOL)file:(NSString*)aPath isNewerThanFile:(NSString*)bPath +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* __autoreleasing error = nil; + + NSDictionary* aPathAttribs = [fileManager attributesOfItemAtPath:aPath error:&error]; + NSDictionary* bPathAttribs = [fileManager attributesOfItemAtPath:bPath error:&error]; + + NSDate* aPathModDate = [aPathAttribs objectForKey:NSFileModificationDate]; + NSDate* bPathModDate = [bPathAttribs objectForKey:NSFileModificationDate]; + + if ((nil == aPathModDate) && (nil == bPathModDate)) { + return NO; + } + + return [aPathModDate compare:bPathModDate] == NSOrderedDescending || bPathModDate == nil; +} + +- (BOOL)item:(NSString*)aPath isNewerThanItem:(NSString*)bPath +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + + BOOL aPathIsDir = NO, bPathIsDir = NO; + BOOL aPathExists = [fileManager fileExistsAtPath:aPath isDirectory:&aPathIsDir]; + + [fileManager fileExistsAtPath:bPath isDirectory:&bPathIsDir]; + + if (!aPathExists) { + return NO; + } + + if (!(aPathIsDir && bPathIsDir)) { // just a file + return [self file:aPath isNewerThanFile:bPath]; + } + + // essentially we want rsync here, but have to settle for our poor man's implementation + // we get the files in aPath, and see if it is newer than the file in bPath + // (it is newer if it doesn't exist in bPath) if we encounter the FIRST file that is newer, + // we return YES + NSDirectoryEnumerator* directoryEnumerator = [fileManager enumeratorAtPath:aPath]; + NSString* path; + + while ((path = [directoryEnumerator nextObject])) { + NSString* aPathFile = [aPath stringByAppendingPathComponent:path]; + NSString* bPathFile = [bPath stringByAppendingPathComponent:path]; + + BOOL isNewer = [self file:aPathFile isNewerThanFile:bPathFile]; + if (isNewer) { + return YES; + } + } + + return NO; +} + +- (BOOL)shouldBackup +{ + return [self item:self.original isNewerThanItem:self.backup]; +} + +- (BOOL)shouldRestore +{ + return [self item:self.backup isNewerThanItem:self.original]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewDelegate.h b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewDelegate.h new file mode 100755 index 0000000..d77f191 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewDelegate.h @@ -0,0 +1,41 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVAvailability.h" + +/** + * Distinguishes top-level navigations from sub-frame navigations. + * shouldStartLoadWithRequest is called for every request, but didStartLoad + * and didFinishLoad is called only for top-level navigations. + * Relevant bug: CB-2389 + */ +@interface CDVUIWebViewDelegate : NSObject { + __weak NSObject * _delegate; + NSInteger _loadCount; + NSInteger _state; + NSInteger _curLoadToken; + NSInteger _loadStartPollCount; +} + +- (id)initWithDelegate:(NSObject *)delegate; + +- (BOOL)request:(NSURLRequest*)newRequest isEqualToRequestAfterStrippingFragments:(NSURLRequest*)originalRequest; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewDelegate.m b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewDelegate.m new file mode 100755 index 0000000..0af97df --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewDelegate.m @@ -0,0 +1,400 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +// +// Testing shows: +// +// In all cases, webView.request.URL is the previous page's URL (or empty) during the didStartLoad callback. +// When loading a page with a redirect: +// 1. shouldStartLoading (requestURL is target page) +// 2. didStartLoading +// 3. shouldStartLoading (requestURL is redirect target) +// 4. didFinishLoad (request.URL is redirect target) +// +// Note the lack of a second didStartLoading ** +// +// When loading a page with iframes: +// 1. shouldStartLoading (requestURL is main page) +// 2. didStartLoading +// 3. shouldStartLoading (requestURL is one of the iframes) +// 4. didStartLoading +// 5. didFinishLoad +// 6. didFinishLoad +// +// Note there is no way to distinguish which didFinishLoad maps to which didStartLoad ** +// +// Loading a page by calling window.history.go(-1): +// 1. didStartLoading +// 2. didFinishLoad +// +// Note the lack of a shouldStartLoading call ** +// Actually - this is fixed on iOS6. iOS6 has a shouldStart. ** +// +// Loading a page by calling location.reload() +// 1. shouldStartLoading +// 2. didStartLoading +// 3. didFinishLoad +// +// Loading a page with an iframe that fails to load: +// 1. shouldStart (main page) +// 2. didStart +// 3. shouldStart (iframe) +// 4. didStart +// 5. didFailWithError +// 6. didFinish +// +// Loading a page with an iframe that fails to load due to an invalid URL: +// 1. shouldStart (main page) +// 2. didStart +// 3. shouldStart (iframe) +// 5. didFailWithError +// 6. didFinish +// +// This case breaks our logic since there is a missing didStart. To prevent this, +// we check URLs in shouldStart and return NO if they are invalid. +// +// Loading a page with an invalid URL +// 1. shouldStart (main page) +// 2. didFailWithError +// +// TODO: Record order when page is re-navigated before the first navigation finishes. +// + +#import "CDVUIWebViewDelegate.h" + +// #define VerboseLog NSLog +#define VerboseLog(...) do { \ +} while (0) + +typedef enum { + STATE_IDLE = 0, + STATE_WAITING_FOR_LOAD_START = 1, + STATE_WAITING_FOR_LOAD_FINISH = 2, + STATE_IOS5_POLLING_FOR_LOAD_START = 3, + STATE_IOS5_POLLING_FOR_LOAD_FINISH = 4, + STATE_CANCELLED = 5 +} State; + +static NSString *stripFragment(NSString* url) +{ + NSRange r = [url rangeOfString:@"#"]; + + if (r.location == NSNotFound) { + return url; + } + return [url substringToIndex:r.location]; +} + +@implementation CDVUIWebViewDelegate + +- (id)initWithDelegate:(NSObject *)delegate +{ + self = [super init]; + if (self != nil) { + _delegate = delegate; + _loadCount = -1; + _state = STATE_IDLE; + } + return self; +} + +- (BOOL)request:(NSURLRequest*)newRequest isEqualToRequestAfterStrippingFragments:(NSURLRequest*)originalRequest +{ + if (originalRequest.URL && newRequest.URL) { + NSString* originalRequestUrl = [originalRequest.URL absoluteString]; + NSString* newRequestUrl = [newRequest.URL absoluteString]; + + NSString* baseOriginalRequestUrl = stripFragment(originalRequestUrl); + NSString* baseNewRequestUrl = stripFragment(newRequestUrl); + return [baseOriginalRequestUrl isEqualToString:baseNewRequestUrl]; + } + + return NO; +} + +- (BOOL)isPageLoaded:(UIWebView*)webView +{ + NSString* readyState = [webView stringByEvaluatingJavaScriptFromString:@"document.readyState"]; + + return [readyState isEqualToString:@"loaded"] || [readyState isEqualToString:@"complete"]; +} + +- (BOOL)isJsLoadTokenSet:(UIWebView*)webView +{ + NSString* loadToken = [webView stringByEvaluatingJavaScriptFromString:@"window.__cordovaLoadToken"]; + + return [[NSString stringWithFormat:@"%ld", (long)_curLoadToken] isEqualToString:loadToken]; +} + +- (void)setLoadToken:(UIWebView*)webView +{ + _curLoadToken += 1; + [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.__cordovaLoadToken=%ld", (long)_curLoadToken]]; +} + +- (NSString*)evalForCurrentURL:(UIWebView*)webView +{ + return [webView stringByEvaluatingJavaScriptFromString:@"location.href"]; +} + +- (void)pollForPageLoadStart:(UIWebView*)webView +{ + if (_state != STATE_IOS5_POLLING_FOR_LOAD_START) { + return; + } + if (![self isJsLoadTokenSet:webView]) { + VerboseLog(@"Polled for page load start. result = YES!"); + _state = STATE_IOS5_POLLING_FOR_LOAD_FINISH; + [self setLoadToken:webView]; + if ([_delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [_delegate webViewDidStartLoad:webView]; + } + [self pollForPageLoadFinish:webView]; + } else { + VerboseLog(@"Polled for page load start. result = NO"); + // Poll only for 1 second, and then fall back on checking only when delegate methods are called. + ++_loadStartPollCount; + if (_loadStartPollCount < (1000 * .05)) { + [self performSelector:@selector(pollForPageLoadStart:) withObject:webView afterDelay:.05]; + } + } +} + +- (void)pollForPageLoadFinish:(UIWebView*)webView +{ + if (_state != STATE_IOS5_POLLING_FOR_LOAD_FINISH) { + return; + } + if ([self isPageLoaded:webView]) { + VerboseLog(@"Polled for page load finish. result = YES!"); + _state = STATE_IDLE; + if ([_delegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { + [_delegate webViewDidFinishLoad:webView]; + } + } else { + VerboseLog(@"Polled for page load finish. result = NO"); + [self performSelector:@selector(pollForPageLoadFinish:) withObject:webView afterDelay:.05]; + } +} + +- (BOOL)shouldLoadRequest:(NSURLRequest*)request +{ + NSString* scheme = [[request URL] scheme]; + NSArray* allowedSchemes = [NSArray arrayWithObjects:@"mailto",@"tel",@"blob",@"sms",@"data", nil]; + if([allowedSchemes containsObject:scheme]) { + return YES; + } + else { + return [NSURLConnection canHandleRequest:request]; + } +} + +- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL shouldLoad = YES; + + if ([_delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { + shouldLoad = [_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; + } + + VerboseLog(@"webView shouldLoad=%d (before) state=%d loadCount=%d URL=%@", shouldLoad, _state, _loadCount, request.URL); + + if (shouldLoad) { + // When devtools refresh occurs, it blindly uses the same request object. If a history.replaceState() has occured, then + // mainDocumentURL != URL even though it's a top-level navigation. + BOOL isDevToolsRefresh = (request == webView.request); + BOOL isTopLevelNavigation = isDevToolsRefresh || [request.URL isEqual:[request mainDocumentURL]]; + if (isTopLevelNavigation) { + // Ignore hash changes that don't navigate to a different page. + // webView.request does actually update when history.replaceState() gets called. + if ([self request:request isEqualToRequestAfterStrippingFragments:webView.request]) { + NSString* prevURL = [self evalForCurrentURL:webView]; + if ([prevURL isEqualToString:[request.URL absoluteString]]) { + VerboseLog(@"Page reload detected."); + } else { + VerboseLog(@"Detected hash change shouldLoad"); + return shouldLoad; + } + } + + switch (_state) { + case STATE_WAITING_FOR_LOAD_FINISH: + // Redirect case. + // We expect loadCount == 1. + if (_loadCount != 1) { + NSLog(@"CDVWebViewDelegate: Detected redirect when loadCount=%ld", (long)_loadCount); + } + break; + + case STATE_IDLE: + case STATE_IOS5_POLLING_FOR_LOAD_START: + case STATE_CANCELLED: + // Page navigation start. + _loadCount = 0; + _state = STATE_WAITING_FOR_LOAD_START; + break; + + default: + { + NSString* description = [NSString stringWithFormat:@"CDVWebViewDelegate: Navigation started when state=%ld", (long)_state]; + NSLog(@"%@", description); + _loadCount = 0; + _state = STATE_WAITING_FOR_LOAD_START; + if ([_delegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { + NSDictionary* errorDictionary = @{NSLocalizedDescriptionKey : description}; + NSError* error = [[NSError alloc] initWithDomain:@"CDVUIWebViewDelegate" code:1 userInfo:errorDictionary]; + [_delegate webView:webView didFailLoadWithError:error]; + } + } + } + } else { + // Deny invalid URLs so that we don't get the case where we go straight from + // webViewShouldLoad -> webViewDidFailLoad (messes up _loadCount). + shouldLoad = shouldLoad && [self shouldLoadRequest:request]; + } + VerboseLog(@"webView shouldLoad=%d (after) isTopLevelNavigation=%d state=%d loadCount=%d", shouldLoad, isTopLevelNavigation, _state, _loadCount); + } + return shouldLoad; +} + +- (void)webViewDidStartLoad:(UIWebView*)webView +{ + VerboseLog(@"webView didStartLoad (before). state=%d loadCount=%d", _state, _loadCount); + BOOL fireCallback = NO; + switch (_state) { + case STATE_IDLE: + break; + + case STATE_CANCELLED: + fireCallback = YES; + _state = STATE_WAITING_FOR_LOAD_FINISH; + _loadCount += 1; + break; + + case STATE_WAITING_FOR_LOAD_START: + if (_loadCount != 0) { + NSLog(@"CDVWebViewDelegate: Unexpected loadCount in didStart. count=%ld", (long)_loadCount); + } + fireCallback = YES; + _state = STATE_WAITING_FOR_LOAD_FINISH; + _loadCount = 1; + break; + + case STATE_WAITING_FOR_LOAD_FINISH: + _loadCount += 1; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_START: + [self pollForPageLoadStart:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_FINISH: + [self pollForPageLoadFinish:webView]; + break; + + default: + NSLog(@"CDVWebViewDelegate: Unexpected didStart with state=%ld loadCount=%ld", (long)_state, (long)_loadCount); + } + VerboseLog(@"webView didStartLoad (after). state=%d loadCount=%d fireCallback=%d", _state, _loadCount, fireCallback); + if (fireCallback && [_delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [_delegate webViewDidStartLoad:webView]; + } +} + +- (void)webViewDidFinishLoad:(UIWebView*)webView +{ + VerboseLog(@"webView didFinishLoad (before). state=%d loadCount=%d", _state, _loadCount); + BOOL fireCallback = NO; + switch (_state) { + case STATE_IDLE: + break; + + case STATE_WAITING_FOR_LOAD_START: + NSLog(@"CDVWebViewDelegate: Unexpected didFinish while waiting for load start."); + break; + + case STATE_WAITING_FOR_LOAD_FINISH: + if (_loadCount == 1) { + fireCallback = YES; + _state = STATE_IDLE; + } + _loadCount -= 1; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_START: + [self pollForPageLoadStart:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_FINISH: + [self pollForPageLoadFinish:webView]; + break; + } + VerboseLog(@"webView didFinishLoad (after). state=%d loadCount=%d fireCallback=%d", _state, _loadCount, fireCallback); + if (fireCallback && [_delegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { + [_delegate webViewDidFinishLoad:webView]; + } +} + +- (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error +{ + VerboseLog(@"webView didFailLoad (before). state=%d loadCount=%d", _state, _loadCount); + BOOL fireCallback = NO; + + switch (_state) { + case STATE_IDLE: + break; + + case STATE_WAITING_FOR_LOAD_START: + if ([error code] == NSURLErrorCancelled) { + _state = STATE_CANCELLED; + } else { + _state = STATE_IDLE; + } + fireCallback = YES; + break; + + case STATE_WAITING_FOR_LOAD_FINISH: + if ([error code] != NSURLErrorCancelled) { + if (_loadCount == 1) { + _state = STATE_IDLE; + fireCallback = YES; + } + _loadCount = -1; + } else { + fireCallback = YES; + _state = STATE_CANCELLED; + _loadCount -= 1; + } + break; + + case STATE_IOS5_POLLING_FOR_LOAD_START: + [self pollForPageLoadStart:webView]; + break; + + case STATE_IOS5_POLLING_FOR_LOAD_FINISH: + [self pollForPageLoadFinish:webView]; + break; + } + VerboseLog(@"webView didFailLoad (after). state=%d loadCount=%d, fireCallback=%d", _state, _loadCount, fireCallback); + if (fireCallback && [_delegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { + [_delegate webView:webView didFailLoadWithError:error]; + } +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewEngine.h b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewEngine.h new file mode 100755 index 0000000..6a9ee77 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewEngine.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" +#import "CDVWebViewEngineProtocol.h" + +@interface CDVUIWebViewEngine : CDVPlugin + +@property (nonatomic, strong, readonly) id uiWebViewDelegate; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewEngine.m b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewEngine.m new file mode 100755 index 0000000..c283e18 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewEngine.m @@ -0,0 +1,197 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVUIWebViewEngine.h" +#import "CDVUIWebViewDelegate.h" +#import "CDVUIWebViewNavigationDelegate.h" +#import "NSDictionary+CordovaPreferences.h" + +#import + +@interface CDVUIWebViewEngine () + +@property (nonatomic, strong, readwrite) UIView* engineWebView; +@property (nonatomic, strong, readwrite) id uiWebViewDelegate; +@property (nonatomic, strong, readwrite) CDVUIWebViewNavigationDelegate* navWebViewDelegate; + +@end + +@implementation CDVUIWebViewEngine + +@synthesize engineWebView = _engineWebView; + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super init]; + if (self) { + self.engineWebView = [[UIWebView alloc] initWithFrame:frame]; + NSLog(@"Using UIWebView"); + } + + return self; +} + +- (void)pluginInitialize +{ + // viewController would be available now. we attempt to set all possible delegates to it, by default + + UIWebView* uiWebView = (UIWebView*)_engineWebView; + + if ([self.viewController conformsToProtocol:@protocol(UIWebViewDelegate)]) { + self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:(id )self.viewController]; + uiWebView.delegate = self.uiWebViewDelegate; + } else { + self.navWebViewDelegate = [[CDVUIWebViewNavigationDelegate alloc] initWithEnginePlugin:self]; + self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self.navWebViewDelegate]; + uiWebView.delegate = self.uiWebViewDelegate; + } + + [self updateSettings:self.commandDelegate.settings]; +} + +- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void (^)(id, NSError*))completionHandler +{ + NSString* ret = [(UIWebView*)_engineWebView stringByEvaluatingJavaScriptFromString:javaScriptString]; + + if (completionHandler) { + completionHandler(ret, nil); + } +} + +- (id)loadRequest:(NSURLRequest*)request +{ + [(UIWebView*)_engineWebView loadRequest:request]; + return nil; +} + +- (id)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL +{ + [(UIWebView*)_engineWebView loadHTMLString:string baseURL:baseURL]; + return nil; +} + +- (NSURL*)URL +{ + return [[(UIWebView*)_engineWebView request] URL]; +} + +- (BOOL) canLoadRequest:(NSURLRequest*)request +{ + return (request != nil); +} + +- (void)updateSettings:(NSDictionary*)settings +{ + UIWebView* uiWebView = (UIWebView*)_engineWebView; + + uiWebView.scalesPageToFit = [settings cordovaBoolSettingForKey:@"EnableViewportScale" defaultValue:NO]; + uiWebView.allowsInlineMediaPlayback = [settings cordovaBoolSettingForKey:@"AllowInlineMediaPlayback" defaultValue:NO]; + uiWebView.mediaPlaybackRequiresUserAction = [settings cordovaBoolSettingForKey:@"MediaPlaybackRequiresUserAction" defaultValue:YES]; + uiWebView.mediaPlaybackAllowsAirPlay = [settings cordovaBoolSettingForKey:@"MediaPlaybackAllowsAirPlay" defaultValue:YES]; + uiWebView.keyboardDisplayRequiresUserAction = [settings cordovaBoolSettingForKey:@"KeyboardDisplayRequiresUserAction" defaultValue:YES]; + uiWebView.suppressesIncrementalRendering = [settings cordovaBoolSettingForKey:@"SuppressesIncrementalRendering" defaultValue:NO]; + uiWebView.gapBetweenPages = [settings cordovaFloatSettingForKey:@"GapBetweenPages" defaultValue:0.0]; + uiWebView.pageLength = [settings cordovaFloatSettingForKey:@"PageLength" defaultValue:0.0]; + + id prefObj = nil; + + // By default, DisallowOverscroll is false (thus bounce is allowed) + BOOL bounceAllowed = !([settings cordovaBoolSettingForKey:@"DisallowOverscroll" defaultValue:NO]); + + // prevent webView from bouncing + if (!bounceAllowed) { + if ([uiWebView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[uiWebView scrollView]).bounces = NO; + } else { + for (id subview in self.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + NSString* decelerationSetting = [settings cordovaSettingForKey:@"UIWebViewDecelerationSpeed"]; + if (![@"fast" isEqualToString:decelerationSetting]) { + [uiWebView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal]; + } + + NSInteger paginationBreakingMode = 0; // default - UIWebPaginationBreakingModePage + prefObj = [settings cordovaSettingForKey:@"PaginationBreakingMode"]; + if (prefObj != nil) { + NSArray* validValues = @[@"page", @"column"]; + NSString* prefValue = [validValues objectAtIndex:0]; + + if ([prefObj isKindOfClass:[NSString class]]) { + prefValue = prefObj; + } + + paginationBreakingMode = [validValues indexOfObject:[prefValue lowercaseString]]; + if (paginationBreakingMode == NSNotFound) { + paginationBreakingMode = 0; + } + } + uiWebView.paginationBreakingMode = paginationBreakingMode; + + NSInteger paginationMode = 0; // default - UIWebPaginationModeUnpaginated + prefObj = [settings cordovaSettingForKey:@"PaginationMode"]; + if (prefObj != nil) { + NSArray* validValues = @[@"unpaginated", @"lefttoright", @"toptobottom", @"bottomtotop", @"righttoleft"]; + NSString* prefValue = [validValues objectAtIndex:0]; + + if ([prefObj isKindOfClass:[NSString class]]) { + prefValue = prefObj; + } + + paginationMode = [validValues indexOfObject:[prefValue lowercaseString]]; + if (paginationMode == NSNotFound) { + paginationMode = 0; + } + } + uiWebView.paginationMode = paginationMode; +} + +- (void)updateWithInfo:(NSDictionary*)info +{ + UIWebView* uiWebView = (UIWebView*)_engineWebView; + + id uiWebViewDelegate = [info objectForKey:kCDVWebViewEngineUIWebViewDelegate]; + NSDictionary* settings = [info objectForKey:kCDVWebViewEngineWebViewPreferences]; + + if (uiWebViewDelegate && + [uiWebViewDelegate conformsToProtocol:@protocol(UIWebViewDelegate)]) { + self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:(id )self.viewController]; + uiWebView.delegate = self.uiWebViewDelegate; + } + + if (settings && [settings isKindOfClass:[NSDictionary class]]) { + [self updateSettings:settings]; + } +} + +// This forwards the methods that are in the header that are not implemented here. +// Both WKWebView and UIWebView implement the below: +// loadHTMLString:baseURL: +// loadRequest: +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + return _engineWebView; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate.h b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate.h new file mode 100755 index 0000000..9138deb --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate.h @@ -0,0 +1,29 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVUIWebViewEngine.h" + +@interface CDVUIWebViewNavigationDelegate : NSObject + +@property (nonatomic, weak) CDVPlugin* enginePlugin; + +- (instancetype)initWithEnginePlugin:(CDVPlugin*)enginePlugin; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate.m b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate.m new file mode 100755 index 0000000..6e31659 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Private/Plugins/CDVUIWebViewEngine/CDVUIWebViewNavigationDelegate.m @@ -0,0 +1,151 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVUIWebViewNavigationDelegate.h" +#import +#import +#import +#import + +@implementation CDVUIWebViewNavigationDelegate + +- (instancetype)initWithEnginePlugin:(CDVPlugin*)theEnginePlugin +{ + self = [super init]; + if (self) { + self.enginePlugin = theEnginePlugin; + } + + return self; +} + +/** + When web application loads Add stuff to the DOM, mainly the user-defined settings from the Settings.plist file, and + the device's data such as device ID, platform version, etc. + */ +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + NSLog(@"Resetting plugins due to page load."); + CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController; + + [vc.commandQueue resetRequestId]; + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginResetNotification object:self.enginePlugin.webView]]; +} + +/** + Called when the webview finishes loading. This stops the activity view. + */ +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + NSLog(@"Finished load of: %@", theWebView.request.URL); + CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController; + + // It's safe to release the lock even if this is just a sub-frame that's finished loading. + [CDVUserAgentUtil releaseLock:vc.userAgentLockToken]; + + /* + * Hide the Top Activity THROBBER in the Battery Bar + */ + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPageDidLoadNotification object:self.enginePlugin.webView]]; +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController; + + [CDVUserAgentUtil releaseLock:vc.userAgentLockToken]; + + NSString* message = [NSString stringWithFormat:@"Failed to load webpage with error: %@", [error localizedDescription]]; + NSLog(@"%@", message); + + NSURL* errorUrl = vc.errorURL; + if (errorUrl) { + errorUrl = [NSURL URLWithString:[NSString stringWithFormat:@"?error=%@", [message stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] relativeToURL:errorUrl]; + NSLog(@"%@", [errorUrl absoluteString]); + [theWebView loadRequest:[NSURLRequest requestWithURL:errorUrl]]; + } +} + +- (BOOL)defaultResourcePolicyForURL:(NSURL*)url +{ + /* + * If a URL is being loaded that's a file url, just load it internally + */ + if ([url isFileURL]) { + return YES; + } + + return NO; +} + +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + NSURL* url = [request URL]; + CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController; + + /* + * Execute any commands queued with cordova.exec() on the JS side. + * The part of the URL after gap:// is irrelevant. + */ + if ([[url scheme] isEqualToString:@"gap"]) { + [vc.commandQueue fetchCommandsFromJs]; + // The delegate is called asynchronously in this case, so we don't have to use + // flushCommandQueueWithDelayedJs (setTimeout(0)) as we do with hash changes. + [vc.commandQueue executePending]; + return NO; + } + + /* + * Give plugins the chance to handle the url + */ + BOOL anyPluginsResponded = NO; + BOOL shouldAllowRequest = NO; + + for (NSString* pluginName in vc.pluginObjects) { + CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName]; + SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:"); + if ([plugin respondsToSelector:selector]) { + anyPluginsResponded = YES; + shouldAllowRequest = (((BOOL (*)(id, SEL, id, int))objc_msgSend)(plugin, selector, request, navigationType)); + if (!shouldAllowRequest) { + break; + } + } + } + + if (anyPluginsResponded) { + return shouldAllowRequest; + } + + /* + * Handle all other types of urls (tel:, sms:), and requests to load a url in the main webview. + */ + BOOL shouldAllowNavigation = [self defaultResourcePolicyForURL:url]; + if (shouldAllowNavigation) { + return YES; + } else { + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + } + + return NO; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDV.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDV.h new file mode 100755 index 0000000..bfc3e44 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDV.h @@ -0,0 +1,34 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVAvailability.h" +#import "CDVAvailabilityDeprecated.h" +#import "CDVAppDelegate.h" +#import "CDVPlugin.h" +#import "CDVPluginResult.h" +#import "CDVViewController.h" +#import "CDVCommandDelegate.h" +#import "CDVURLProtocol.h" +#import "CDVInvokedUrlCommand.h" +#import "CDVWhitelist.h" +#import "CDVPlugin.h" +#import "CDVPluginResult.h" +#import "CDVScreenOrientationDelegate.h" +#import "CDVTimer.h" +#import "CDVUserAgentUtil.h" diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVAppDelegate.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAppDelegate.h new file mode 100755 index 0000000..de5b518 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAppDelegate.h @@ -0,0 +1,28 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVViewController.h" + +@interface CDVAppDelegate : NSObject {} + +@property (nonatomic, strong) IBOutlet UIWindow* window; +@property (nonatomic, strong) IBOutlet CDVViewController* viewController; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVAppDelegate.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAppDelegate.m new file mode 100755 index 0000000..13c2e7b --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAppDelegate.m @@ -0,0 +1,105 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVAppDelegate.h" + +@implementation CDVAppDelegate + +@synthesize window, viewController; + +- (id)init +{ + /** If you need to do any extra app-specific initialization, you can do it here + * -jm + **/ + NSHTTPCookieStorage* cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + + [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; + + int cacheSizeMemory = 8 * 1024 * 1024; // 8MB + int cacheSizeDisk = 32 * 1024 * 1024; // 32MB + NSURLCache* sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:@"nsurlcache"]; + [NSURLCache setSharedURLCache:sharedCache]; + + self = [super init]; + return self; +} + +#pragma mark UIApplicationDelegate implementation + +/** + * This is main kick off after the app inits, the views and Settings are setup here. (preferred - iOS4 and up) + */ +- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions +{ + CGRect screenBounds = [[UIScreen mainScreen] bounds]; + + self.window = [[UIWindow alloc] initWithFrame:screenBounds]; + self.window.autoresizesSubviews = YES; + + // only set if not already set in subclass + if (self.viewController == nil) { + self.viewController = [[CDVViewController alloc] init]; + } + + // Set your app's start page by setting the tag in config.xml. + // If necessary, uncomment the line below to override it. + // self.viewController.startPage = @"index.html"; + + // NOTE: To customize the view's frame size (which defaults to full screen), override + // [self.viewController viewWillAppear:] in your view controller. + + self.window.rootViewController = self.viewController; + [self.window makeKeyAndVisible]; + + return YES; +} + +// this happens while we are running ( in the background, or from within our own app ) +// only valid if 40x-Info.plist specifies a protocol to handle +- (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation +{ + if (!url) { + return NO; + } + + // all plugins will get the notification, and their handlers will be called + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + + return YES; +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED < 90000 +- (NSUInteger)application:(UIApplication*)application supportedInterfaceOrientationsForWindow:(UIWindow*)window +#else +- (UIInterfaceOrientationMask)application:(UIApplication*)application supportedInterfaceOrientationsForWindow:(UIWindow*)window +#endif +{ + // iPhone doesn't support upside down by default, while the iPad does. Override to allow all orientations always, and let the root view controller decide what's allowed (the supported orientations mask gets intersected). + NSUInteger supportedInterfaceOrientations = (1 << UIInterfaceOrientationPortrait) | (1 << UIInterfaceOrientationLandscapeLeft) | (1 << UIInterfaceOrientationLandscapeRight) | (1 << UIInterfaceOrientationPortraitUpsideDown); + + return supportedInterfaceOrientations; +} + +- (void)applicationDidReceiveMemoryWarning:(UIApplication*)application +{ + [[NSURLCache sharedURLCache] removeAllCachedResponses]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVAvailability.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAvailability.h new file mode 100755 index 0000000..13cbac0 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAvailability.h @@ -0,0 +1,102 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVAvailabilityDeprecated.h" + +#define __CORDOVA_IOS__ + +#define __CORDOVA_0_9_6 906 +#define __CORDOVA_1_0_0 10000 +#define __CORDOVA_1_1_0 10100 +#define __CORDOVA_1_2_0 10200 +#define __CORDOVA_1_3_0 10300 +#define __CORDOVA_1_4_0 10400 +#define __CORDOVA_1_4_1 10401 +#define __CORDOVA_1_5_0 10500 +#define __CORDOVA_1_6_0 10600 +#define __CORDOVA_1_6_1 10601 +#define __CORDOVA_1_7_0 10700 +#define __CORDOVA_1_8_0 10800 +#define __CORDOVA_1_8_1 10801 +#define __CORDOVA_1_9_0 10900 +#define __CORDOVA_2_0_0 20000 +#define __CORDOVA_2_1_0 20100 +#define __CORDOVA_2_2_0 20200 +#define __CORDOVA_2_3_0 20300 +#define __CORDOVA_2_4_0 20400 +#define __CORDOVA_2_5_0 20500 +#define __CORDOVA_2_6_0 20600 +#define __CORDOVA_2_7_0 20700 +#define __CORDOVA_2_8_0 20800 +#define __CORDOVA_2_9_0 20900 +#define __CORDOVA_3_0_0 30000 +#define __CORDOVA_3_1_0 30100 +#define __CORDOVA_3_2_0 30200 +#define __CORDOVA_3_3_0 30300 +#define __CORDOVA_3_4_0 30400 +#define __CORDOVA_3_4_1 30401 +#define __CORDOVA_3_5_0 30500 +#define __CORDOVA_3_6_0 30600 +#define __CORDOVA_3_7_0 30700 +#define __CORDOVA_3_8_0 30800 +#define __CORDOVA_3_9_0 30900 +#define __CORDOVA_3_9_1 30901 +#define __CORDOVA_3_9_2 30902 +#define __CORDOVA_4_0_0 40000 +#define __CORDOVA_4_0_1 40001 +#define __CORDOVA_4_1_0 40100 +#define __CORDOVA_4_1_1 40101 +/* coho:next-version,insert-before */ +#define __CORDOVA_NA 99999 /* not available */ + +/* + #if CORDOVA_VERSION_MIN_REQUIRED >= __CORDOVA_4_0_0 + // do something when its at least 4.0.0 + #else + // do something else (non 4.0.0) + #endif + */ +#ifndef CORDOVA_VERSION_MIN_REQUIRED + /* coho:next-version-min-required,replace-after */ + #define CORDOVA_VERSION_MIN_REQUIRED __CORDOVA_4_1_1 +#endif + +/* + Returns YES if it is at least version specified as NSString(X) + Usage: + if (IsAtLeastiOSVersion(@"5.1")) { + // do something for iOS 5.1 or greater + } + */ +#define IsAtLeastiOSVersion(X) ([[[UIDevice currentDevice] systemVersion] compare:X options:NSNumericSearch] != NSOrderedAscending) + +/* Return the string version of the decimal version */ +#define CDV_VERSION [NSString stringWithFormat:@"%d.%d.%d", \ + (CORDOVA_VERSION_MIN_REQUIRED / 10000), \ + (CORDOVA_VERSION_MIN_REQUIRED % 10000) / 100, \ + (CORDOVA_VERSION_MIN_REQUIRED % 10000) % 100] + +// Enable this to log all exec() calls. +#define CDV_ENABLE_EXEC_LOGGING 0 +#if CDV_ENABLE_EXEC_LOGGING + #define CDV_EXEC_LOG NSLog +#else + #define CDV_EXEC_LOG(...) do { \ +} while (NO) +#endif diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVAvailabilityDeprecated.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAvailabilityDeprecated.h new file mode 100755 index 0000000..abf7a16 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVAvailabilityDeprecated.h @@ -0,0 +1,26 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +#ifdef __clang__ + #define CDV_DEPRECATED(version, msg) __attribute__((deprecated("Deprecated in Cordova " #version ". " msg))) +#else + #define CDV_DEPRECATED(version, msg) __attribute__((deprecated())) +#endif diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegate.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegate.h new file mode 100755 index 0000000..3d9d90c --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegate.h @@ -0,0 +1,51 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVAvailability.h" +#import "CDVInvokedUrlCommand.h" + +@class CDVPlugin; +@class CDVPluginResult; +@class CDVWhitelist; + +typedef NSURL* (^ UrlTransformerBlock)(NSURL*); + +@protocol CDVCommandDelegate + +@property (nonatomic, readonly) NSDictionary* settings; +@property (nonatomic, copy) UrlTransformerBlock urlTransformer; + +- (NSString*)pathForResource:(NSString*)resourcepath; +- (id)getCommandInstance:(NSString*)pluginName; + +// Sends a plugin result to the JS. This is thread-safe. +- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId; +// Evaluates the given JS. This is thread-safe. +- (void)evalJs:(NSString*)js; +// Can be used to evaluate JS right away instead of scheduling it on the run-loop. +// This is required for dispatch resign and pause events, but should not be used +// without reason. Without the run-loop delay, alerts used in JS callbacks may result +// in dead-lock. This method must be called from the UI thread. +- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop; +// Runs the given block on a background thread using a shared thread-pool. +- (void)runInBackground:(void (^)())block; +// Returns the User-Agent of the associated UIWebView. +- (NSString*)userAgent; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegateImpl.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegateImpl.h new file mode 100755 index 0000000..0531134 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegateImpl.h @@ -0,0 +1,36 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVCommandDelegate.h" + +@class CDVViewController; +@class CDVCommandQueue; + +@interface CDVCommandDelegateImpl : NSObject { + @private + __weak CDVViewController* _viewController; + NSRegularExpression* _callbackIdPattern; + @protected + __weak CDVCommandQueue* _commandQueue; + BOOL _delayResponses; +} +- (id)initWithViewController:(CDVViewController*)viewController; +- (void)flushCommandQueueWithDelayedJs; +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegateImpl.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegateImpl.m new file mode 100755 index 0000000..be796df --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandDelegateImpl.m @@ -0,0 +1,186 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVCommandDelegateImpl.h" +#import "CDVJSON_private.h" +#import "CDVCommandQueue.h" +#import "CDVPluginResult.h" +#import "CDVViewController.h" + +@implementation CDVCommandDelegateImpl + +@synthesize urlTransformer; + +- (id)initWithViewController:(CDVViewController*)viewController +{ + self = [super init]; + if (self != nil) { + _viewController = viewController; + _commandQueue = _viewController.commandQueue; + + NSError* err = nil; + _callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"[^A-Za-z0-9._-]" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex + NSLog(@"Error: Couldn't initialize regex"); + _callbackIdPattern = nil; + } + } + return self; +} + +- (NSString*)pathForResource:(NSString*)resourcepath +{ + NSBundle* mainBundle = [NSBundle mainBundle]; + NSMutableArray* directoryParts = [NSMutableArray arrayWithArray:[resourcepath componentsSeparatedByString:@"/"]]; + NSString* filename = [directoryParts lastObject]; + + [directoryParts removeLastObject]; + + NSString* directoryPartsJoined = [directoryParts componentsJoinedByString:@"/"]; + NSString* directoryStr = _viewController.wwwFolderName; + + if ([directoryPartsJoined length] > 0) { + directoryStr = [NSString stringWithFormat:@"%@/%@", _viewController.wwwFolderName, [directoryParts componentsJoinedByString:@"/"]]; + } + + return [mainBundle pathForResource:filename ofType:@"" inDirectory:directoryStr]; +} + +- (void)flushCommandQueueWithDelayedJs +{ + _delayResponses = YES; + [_commandQueue executePending]; + _delayResponses = NO; +} + +- (void)evalJsHelper2:(NSString*)js +{ + CDV_EXEC_LOG(@"Exec: evalling: %@", [js substringToIndex:MIN([js length], 160)]); + [_viewController.webViewEngine evaluateJavaScript:js completionHandler:^(id obj, NSError* error) { + // TODO: obj can be something other than string + if ([obj isKindOfClass:[NSString class]]) { + NSString* commandsJSON = (NSString*)obj; + if ([commandsJSON length] > 0) { + CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by chaining."); + } + + [_commandQueue enqueueCommandBatch:commandsJSON]; + [_commandQueue executePending]; + } + }]; +} + +- (void)evalJsHelper:(NSString*)js +{ + // Cycle the run-loop before executing the JS. + // For _delayResponses - + // This ensures that we don't eval JS during the middle of an existing JS + // function (possible since UIWebViewDelegate callbacks can be synchronous). + // For !isMainThread - + // It's a hard error to eval on the non-UI thread. + // For !_commandQueue.currentlyExecuting - + // This works around a bug where sometimes alerts() within callbacks can cause + // dead-lock. + // If the commandQueue is currently executing, then we know that it is safe to + // execute the callback immediately. + // Using (dispatch_get_main_queue()) does *not* fix deadlocks for some reason, + // but performSelectorOnMainThread: does. + if (_delayResponses || ![NSThread isMainThread] || !_commandQueue.currentlyExecuting) { + [self performSelectorOnMainThread:@selector(evalJsHelper2:) withObject:js waitUntilDone:NO]; + } else { + [self evalJsHelper2:js]; + } +} + +- (BOOL)isValidCallbackId:(NSString*)callbackId +{ + if ((callbackId == nil) || (_callbackIdPattern == nil)) { + return NO; + } + + // Disallow if too long or if any invalid characters were found. + if (([callbackId length] > 100) || [_callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return NO; + } + return YES; +} + +- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId +{ + CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status); + // This occurs when there is are no win/fail callbacks for the call. + if ([@"INVALID" isEqualToString:callbackId]) { + return; + } + // This occurs when the callback id is malformed. + if (![self isValidCallbackId:callbackId]) { + NSLog(@"Invalid callback id received by sendPluginResult"); + return; + } + int status = [result.status intValue]; + BOOL keepCallback = [result.keepCallback boolValue]; + NSString* argumentsAsJSON = [result argumentsAsJSON]; + BOOL debug = NO; + +#ifdef DEBUG + debug = YES; +#endif + + NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d, %d)", callbackId, status, argumentsAsJSON, keepCallback, debug]; + + [self evalJsHelper:js]; +} + +- (void)evalJs:(NSString*)js +{ + [self evalJs:js scheduledOnRunLoop:YES]; +} + +- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop +{ + js = [NSString stringWithFormat:@"try{cordova.require('cordova/exec').nativeEvalAndFetch(function(){%@})}catch(e){console.log('exeption nativeEvalAndFetch : '+e);};", js]; + if (scheduledOnRunLoop) { + [self evalJsHelper:js]; + } else { + [self evalJsHelper2:js]; + } +} + +- (id)getCommandInstance:(NSString*)pluginName +{ + return [_viewController getCommandInstance:pluginName]; +} + +- (void)runInBackground:(void (^)())block +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); +} + +- (NSString*)userAgent +{ + return [_viewController userAgent]; +} + +- (NSDictionary*)settings +{ + return _viewController.settings; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandQueue.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandQueue.h new file mode 100755 index 0000000..cb7bd6e --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandQueue.h @@ -0,0 +1,39 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@class CDVInvokedUrlCommand; +@class CDVViewController; + +@interface CDVCommandQueue : NSObject + +@property (nonatomic, readonly) BOOL currentlyExecuting; + +- (id)initWithViewController:(CDVViewController*)viewController; +- (void)dispose; + +- (void)resetRequestId; +- (void)enqueueCommandBatch:(NSString*)batchJSON; + +- (void)fetchCommandsFromJs; +- (void)executePending; +- (BOOL)execute:(CDVInvokedUrlCommand*)command; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandQueue.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandQueue.m new file mode 100755 index 0000000..b78ed83 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVCommandQueue.m @@ -0,0 +1,194 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#include +#import "CDVCommandQueue.h" +#import "CDVViewController.h" +#import "CDVCommandDelegateImpl.h" +#import "CDVJSON_private.h" +#import "CDVDebug.h" + +// Parse JS on the main thread if it's shorter than this. +static const NSInteger JSON_SIZE_FOR_MAIN_THREAD = 4 * 1024; // Chosen arbitrarily. +// Execute multiple commands in one go until this many seconds have passed. +static const double MAX_EXECUTION_TIME = .008; // Half of a 60fps frame. + +@interface CDVCommandQueue () { + NSInteger _lastCommandQueueFlushRequestId; + __weak CDVViewController* _viewController; + NSMutableArray* _queue; + NSTimeInterval _startExecutionTime; +} +@end + +@implementation CDVCommandQueue + +- (BOOL)currentlyExecuting +{ + return _startExecutionTime > 0; +} + +- (id)initWithViewController:(CDVViewController*)viewController +{ + self = [super init]; + if (self != nil) { + _viewController = viewController; + _queue = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dispose +{ + // TODO(agrieve): Make this a zeroing weak ref once we drop support for 4.3. + _viewController = nil; +} + +- (void)resetRequestId +{ + _lastCommandQueueFlushRequestId = 0; +} + +- (void)enqueueCommandBatch:(NSString*)batchJSON +{ + if ([batchJSON length] > 0) { + NSMutableArray* commandBatchHolder = [[NSMutableArray alloc] init]; + [_queue addObject:commandBatchHolder]; + if ([batchJSON length] < JSON_SIZE_FOR_MAIN_THREAD) { + [commandBatchHolder addObject:[batchJSON cdv_JSONObject]]; + } else { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() { + NSMutableArray* result = [batchJSON cdv_JSONObject]; + @synchronized(commandBatchHolder) { + [commandBatchHolder addObject:result]; + } + [self performSelectorOnMainThread:@selector(executePending) withObject:nil waitUntilDone:NO]; + }); + } + } +} + +- (void)fetchCommandsFromJs +{ + __weak CDVCommandQueue* weakSelf = self; + NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()"; + + [_viewController.webViewEngine evaluateJavaScript:js + completionHandler:^(id obj, NSError* error) { + if ((error == nil) && [obj isKindOfClass:[NSString class]]) { + NSString* queuedCommandsJSON = (NSString*)obj; + CDV_EXEC_LOG(@"Exec: Flushed JS->native queue (hadCommands=%d).", [queuedCommandsJSON length] > 0); + [weakSelf enqueueCommandBatch:queuedCommandsJSON]; + // this has to be called here now, because fetchCommandsFromJs is now async (previously: synchronous) + [self executePending]; + } + }]; +} + +- (void)executePending +{ + // Make us re-entrant-safe. + if (_startExecutionTime > 0) { + return; + } + @try { + _startExecutionTime = [NSDate timeIntervalSinceReferenceDate]; + + while ([_queue count] > 0) { + NSMutableArray* commandBatchHolder = _queue[0]; + NSMutableArray* commandBatch = nil; + @synchronized(commandBatchHolder) { + // If the next-up command is still being decoded, wait for it. + if ([commandBatchHolder count] == 0) { + break; + } + commandBatch = commandBatchHolder[0]; + } + + while ([commandBatch count] > 0) { + @autoreleasepool { + // Execute the commands one-at-a-time. + NSArray* jsonEntry = [commandBatch cdv_dequeue]; + if ([commandBatch count] == 0) { + [_queue removeObjectAtIndex:0]; + } + CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry]; + CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName); + + if (![self execute:command]) { +#ifdef DEBUG + NSString* commandJson = [jsonEntry cdv_JSONString]; + static NSUInteger maxLogLength = 1024; + NSString* commandString = ([commandJson length] > maxLogLength) ? + [NSString stringWithFormat : @"%@[...]", [commandJson substringToIndex:maxLogLength]] : + commandJson; + + DLog(@"FAILED pluginJSON = %@", commandString); +#endif + } + } + + // Yield if we're taking too long. + if (([_queue count] > 0) && ([NSDate timeIntervalSinceReferenceDate] - _startExecutionTime > MAX_EXECUTION_TIME)) { + [self performSelector:@selector(executePending) withObject:nil afterDelay:0]; + return; + } + } + } + } @finally + { + _startExecutionTime = 0; + } +} + +- (BOOL)execute:(CDVInvokedUrlCommand*)command +{ + if ((command.className == nil) || (command.methodName == nil)) { + NSLog(@"ERROR: Classname and/or methodName not found for command."); + return NO; + } + + // Fetch an instance of this class + CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className]; + + if (!([obj isKindOfClass:[CDVPlugin class]])) { + NSLog(@"ERROR: Plugin '%@' not found, or is not a CDVPlugin. Check your plugin mapping in config.xml.", command.className); + return NO; + } + BOOL retVal = YES; + double started = [[NSDate date] timeIntervalSince1970] * 1000.0; + // Find the proper selector to call. + NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName]; + SEL normalSelector = NSSelectorFromString(methodName); + if ([obj respondsToSelector:normalSelector]) { + // [obj performSelector:normalSelector withObject:command]; + ((void (*)(id, SEL, id))objc_msgSend)(obj, normalSelector, command); + } else { + // There's no method to call, so throw an error. + NSLog(@"ERROR: Method '%@' not defined in Plugin '%@'", methodName, command.className); + retVal = NO; + } + double elapsed = [[NSDate date] timeIntervalSince1970] * 1000.0 - started; + if (elapsed > 10) { + NSLog(@"THREAD WARNING: ['%@'] took '%f' ms. Plugin should use a background thread.", command.className, elapsed); + } + return retVal; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVConfigParser.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVConfigParser.h new file mode 100755 index 0000000..bae3d0f --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVConfigParser.h @@ -0,0 +1,30 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +@interface CDVConfigParser : NSObject +{ + NSString* featureName; +} + +@property (nonatomic, readonly, strong) NSMutableDictionary* pluginsDict; +@property (nonatomic, readonly, strong) NSMutableDictionary* settings; +@property (nonatomic, readonly, strong) NSMutableArray* startupPluginNames; +@property (nonatomic, readonly, strong) NSString* startPage; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVConfigParser.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVConfigParser.m new file mode 100755 index 0000000..ab32b4a --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVConfigParser.m @@ -0,0 +1,81 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVConfigParser.h" + +@interface CDVConfigParser () + +@property (nonatomic, readwrite, strong) NSMutableDictionary* pluginsDict; +@property (nonatomic, readwrite, strong) NSMutableDictionary* settings; +@property (nonatomic, readwrite, strong) NSMutableArray* startupPluginNames; +@property (nonatomic, readwrite, strong) NSString* startPage; + +@end + +@implementation CDVConfigParser + +@synthesize pluginsDict, settings, startPage, startupPluginNames; + +- (id)init +{ + self = [super init]; + if (self != nil) { + self.pluginsDict = [[NSMutableDictionary alloc] initWithCapacity:30]; + self.settings = [[NSMutableDictionary alloc] initWithCapacity:30]; + self.startupPluginNames = [[NSMutableArray alloc] initWithCapacity:8]; + featureName = nil; + } + return self; +} + +- (void)parser:(NSXMLParser*)parser didStartElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName attributes:(NSDictionary*)attributeDict +{ + if ([elementName isEqualToString:@"preference"]) { + settings[[attributeDict[@"name"] lowercaseString]] = attributeDict[@"value"]; + } else if ([elementName isEqualToString:@"feature"]) { // store feature name to use with correct parameter set + featureName = [attributeDict[@"name"] lowercaseString]; + } else if ((featureName != nil) && [elementName isEqualToString:@"param"]) { + NSString* paramName = [attributeDict[@"name"] lowercaseString]; + id value = attributeDict[@"value"]; + if ([paramName isEqualToString:@"ios-package"]) { + pluginsDict[featureName] = value; + } + BOOL paramIsOnload = ([paramName isEqualToString:@"onload"] && [@"true" isEqualToString : value]); + BOOL attribIsOnload = [@"true" isEqualToString :[attributeDict[@"onload"] lowercaseString]]; + if (paramIsOnload || attribIsOnload) { + [self.startupPluginNames addObject:featureName]; + } + } else if ([elementName isEqualToString:@"content"]) { + self.startPage = attributeDict[@"src"]; + } +} + +- (void)parser:(NSXMLParser*)parser didEndElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName +{ + if ([elementName isEqualToString:@"feature"]) { // no longer handling a feature so release + featureName = nil; + } +} + +- (void)parser:(NSXMLParser*)parser parseErrorOccurred:(NSError*)parseError +{ + NSAssert(NO, @"config.xml parse error line %ld col %ld", (long)[parser lineNumber], (long)[parser columnNumber]); +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVInvokedUrlCommand.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVInvokedUrlCommand.h new file mode 100755 index 0000000..993e0a2 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVInvokedUrlCommand.h @@ -0,0 +1,52 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVInvokedUrlCommand : NSObject { + NSString* _callbackId; + NSString* _className; + NSString* _methodName; + NSArray* _arguments; +} + +@property (nonatomic, readonly) NSArray* arguments; +@property (nonatomic, readonly) NSString* callbackId; +@property (nonatomic, readonly) NSString* className; +@property (nonatomic, readonly) NSString* methodName; + ++ (CDVInvokedUrlCommand*)commandFromJson:(NSArray*)jsonEntry; + +- (id)initWithArguments:(NSArray*)arguments + callbackId:(NSString*)callbackId + className:(NSString*)className + methodName:(NSString*)methodName; + +- (id)initFromJson:(NSArray*)jsonEntry; + +// Returns the argument at the given index. +// If index >= the number of arguments, returns nil. +// If the argument at the given index is NSNull, returns nil. +- (id)argumentAtIndex:(NSUInteger)index; +// Same as above, but returns defaultValue instead of nil. +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue; +// Same as above, but returns defaultValue instead of nil, and if the argument is not of the expected class, returns defaultValue +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue andClass:(Class)aClass; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVInvokedUrlCommand.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVInvokedUrlCommand.m new file mode 100755 index 0000000..5b4281d --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVInvokedUrlCommand.m @@ -0,0 +1,116 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInvokedUrlCommand.h" +#import "CDVJSON_private.h" + +@implementation CDVInvokedUrlCommand + +@synthesize arguments = _arguments; +@synthesize callbackId = _callbackId; +@synthesize className = _className; +@synthesize methodName = _methodName; + ++ (CDVInvokedUrlCommand*)commandFromJson:(NSArray*)jsonEntry +{ + return [[CDVInvokedUrlCommand alloc] initFromJson:jsonEntry]; +} + +- (id)initFromJson:(NSArray*)jsonEntry +{ + id tmp = [jsonEntry objectAtIndex:0]; + NSString* callbackId = tmp == [NSNull null] ? nil : tmp; + NSString* className = [jsonEntry objectAtIndex:1]; + NSString* methodName = [jsonEntry objectAtIndex:2]; + NSMutableArray* arguments = [jsonEntry objectAtIndex:3]; + + return [self initWithArguments:arguments + callbackId:callbackId + className:className + methodName:methodName]; +} + +- (id)initWithArguments:(NSArray*)arguments + callbackId:(NSString*)callbackId + className:(NSString*)className + methodName:(NSString*)methodName +{ + self = [super init]; + if (self != nil) { + _arguments = arguments; + _callbackId = callbackId; + _className = className; + _methodName = methodName; + } + [self massageArguments]; + return self; +} + +- (void)massageArguments +{ + NSMutableArray* newArgs = nil; + + for (NSUInteger i = 0, count = [_arguments count]; i < count; ++i) { + id arg = [_arguments objectAtIndex:i]; + if (![arg isKindOfClass:[NSDictionary class]]) { + continue; + } + NSDictionary* dict = arg; + NSString* type = [dict objectForKey:@"CDVType"]; + if (!type || ![type isEqualToString:@"ArrayBuffer"]) { + continue; + } + NSString* data = [dict objectForKey:@"data"]; + if (!data) { + continue; + } + if (newArgs == nil) { + newArgs = [NSMutableArray arrayWithArray:_arguments]; + _arguments = newArgs; + } + [newArgs replaceObjectAtIndex:i withObject:[[NSData alloc] initWithBase64EncodedString:data options:0]]; + } +} + +- (id)argumentAtIndex:(NSUInteger)index +{ + return [self argumentAtIndex:index withDefault:nil]; +} + +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue +{ + return [self argumentAtIndex:index withDefault:defaultValue andClass:nil]; +} + +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue andClass:(Class)aClass +{ + if (index >= [_arguments count]) { + return defaultValue; + } + id ret = [_arguments objectAtIndex:index]; + if (ret == [NSNull null]) { + ret = defaultValue; + } + if ((aClass != nil) && ![ret isKindOfClass:aClass]) { + ret = defaultValue; + } + return ret; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin+Resources.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin+Resources.h new file mode 100755 index 0000000..cc43b16 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin+Resources.h @@ -0,0 +1,39 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +@interface CDVPlugin (CDVPluginResources) + +/* + This will return the localized string for a key in a .bundle that is named the same as your class + For example, if your plugin class was called Foo, and you have a Spanish localized strings file, it will + try to load the desired key from Foo.bundle/es.lproj/Localizable.strings + */ +- (NSString*)pluginLocalizedString:(NSString*)key; + +/* + This will return the image for a name in a .bundle that is named the same as your class + For example, if your plugin class was called Foo, and you have an image called "bar", + it will try to load the image from Foo.bundle/bar.png (and appropriately named retina versions) + */ +- (UIImage*)pluginImageResource:(NSString*)name; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin+Resources.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin+Resources.m new file mode 100755 index 0000000..5690738 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin+Resources.m @@ -0,0 +1,38 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin+Resources.h" + +@implementation CDVPlugin (CDVPluginResources) + +- (NSString*)pluginLocalizedString:(NSString*)key +{ + NSBundle* bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:NSStringFromClass([self class]) ofType:@"bundle"]]; + + return [bundle localizedStringForKey:(key) value:nil table:nil]; +} + +- (UIImage*)pluginImageResource:(NSString*)name +{ + NSString* resourceIdentifier = [NSString stringWithFormat:@"%@.bundle/%@", NSStringFromClass([self class]), name]; + + return [UIImage imageNamed:resourceIdentifier]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin.h new file mode 100755 index 0000000..54c8afd --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin.h @@ -0,0 +1,69 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import "CDVPluginResult.h" +#import "NSMutableArray+QueueAdditions.h" +#import "CDVCommandDelegate.h" +#import "CDVWebViewEngineProtocol.h" + +@interface UIView (org_apache_cordova_UIView_Extension) + +@property (nonatomic, weak) UIScrollView* scrollView; + +@end + +extern NSString* const CDVPageDidLoadNotification; +extern NSString* const CDVPluginHandleOpenURLNotification; +extern NSString* const CDVPluginResetNotification; +extern NSString* const CDVLocalNotification; +extern NSString* const CDVRemoteNotification; +extern NSString* const CDVRemoteNotificationError; + +@interface CDVPlugin : NSObject {} + +@property (nonatomic, readonly, weak) UIView* webView; +@property (nonatomic, readonly, weak) id webViewEngine; + +@property (nonatomic, weak) UIViewController* viewController; +@property (nonatomic, weak) id commandDelegate; + +@property (readonly, assign) BOOL hasPendingOperation; + +- (void)pluginInitialize; + +- (void)handleOpenURL:(NSNotification*)notification; +- (void)onAppTerminate; +- (void)onMemoryWarning; +- (void)onReset; +- (void)dispose; + +/* + // see initWithWebView implementation + - (void) onPause {} + - (void) onResume {} + - (void) onOrientationWillChange {} + - (void) onOrientationDidChange {} + - (void)didReceiveLocalNotification:(NSNotification *)notification; + */ + +- (id)appDelegate; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin.m new file mode 100755 index 0000000..ac3a8ee --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPlugin.m @@ -0,0 +1,163 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" +#import "CDVPlugin+Private.h" +#import "CDVPlugin+Resources.h" +#import "CDVViewController.h" +#include + +@implementation UIView (org_apache_cordova_UIView_Extension) + +@dynamic scrollView; + +- (UIScrollView*)scrollView +{ + SEL scrollViewSelector = NSSelectorFromString(@"scrollView"); + + if ([self respondsToSelector:scrollViewSelector]) { + return ((id (*)(id, SEL))objc_msgSend)(self, scrollViewSelector); + } + + return nil; +} + +@end + +NSString* const CDVPageDidLoadNotification = @"CDVPageDidLoadNotification"; +NSString* const CDVPluginHandleOpenURLNotification = @"CDVPluginHandleOpenURLNotification"; +NSString* const CDVPluginResetNotification = @"CDVPluginResetNotification"; +NSString* const CDVLocalNotification = @"CDVLocalNotification"; +NSString* const CDVRemoteNotification = @"CDVRemoteNotification"; +NSString* const CDVRemoteNotificationError = @"CDVRemoteNotificationError"; + +@interface CDVPlugin () + +@property (readwrite, assign) BOOL hasPendingOperation; +@property (nonatomic, readwrite, weak) id webViewEngine; + +@end + +@implementation CDVPlugin +@synthesize webViewEngine, viewController, commandDelegate, hasPendingOperation; +@dynamic webView; + +// Do not override these methods. Use pluginInitialize instead. +- (instancetype)initWithWebViewEngine:(id )theWebViewEngine +{ + self = [super init]; + if (self) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppTerminate) name:UIApplicationWillTerminateNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOpenURL:) name:CDVPluginHandleOpenURLNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onReset) name:CDVPluginResetNotification object:theWebViewEngine.engineWebView]; + + self.webViewEngine = theWebViewEngine; + } + return self; +} + +- (void)pluginInitialize +{ + // You can listen to more app notifications, see: + // http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIApplication_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006728-CH3-DontLinkElementID_4 + + // NOTE: if you want to use these, make sure you uncomment the corresponding notification handler + + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationWillChange) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationDidChange) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; + + // Added in 2.3.0 + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveLocalNotification:) name:CDVLocalNotification object:nil]; + + // Added in 2.5.0 + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pageDidLoad:) name:CDVPageDidLoadNotification object:self.webView]; +} + +- (void)dispose +{ + viewController = nil; + commandDelegate = nil; +} + +- (UIView*)webView +{ + if (self.webViewEngine != nil) { + return self.webViewEngine.engineWebView; + } + + return nil; +} + +/* +// NOTE: for onPause and onResume, calls into JavaScript must not call or trigger any blocking UI, like alerts +- (void) onPause {} +- (void) onResume {} +- (void) onOrientationWillChange {} +- (void) onOrientationDidChange {} +*/ + +/* NOTE: calls into JavaScript must not call or trigger any blocking UI, like alerts */ +- (void)handleOpenURL:(NSNotification*)notification +{ + // override to handle urls sent to your app + // register your url schemes in your App-Info.plist + + NSURL* url = [notification object]; + + if ([url isKindOfClass:[NSURL class]]) { + /* Do your thing! */ + } +} + +/* NOTE: calls into JavaScript must not call or trigger any blocking UI, like alerts */ +- (void)onAppTerminate +{ + // override this if you need to do any cleanup on app exit +} + +- (void)onMemoryWarning +{ + // override to remove caches, etc +} + +- (void)onReset +{ + // Override to cancel any long-running requests when the WebView navigates or refreshes. +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; // this will remove all notification unless added using addObserverForName:object:queue:usingBlock: +} + +- (id)appDelegate +{ + return [[UIApplication sharedApplication] delegate]; +} + +// default implementation does nothing, ideally, we are not registered for notification if we aren't going to do anything. +// - (void)didReceiveLocalNotification:(NSNotification *)notification +// { +// // UILocalNotification* localNotification = [notification object]; // get the payload as a LocalNotification +// } + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVPluginResult.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPluginResult.h new file mode 100755 index 0000000..56b8c23 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPluginResult.h @@ -0,0 +1,66 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVAvailability.h" + +typedef enum { + CDVCommandStatus_NO_RESULT = 0, + CDVCommandStatus_OK, + CDVCommandStatus_CLASS_NOT_FOUND_EXCEPTION, + CDVCommandStatus_ILLEGAL_ACCESS_EXCEPTION, + CDVCommandStatus_INSTANTIATION_EXCEPTION, + CDVCommandStatus_MALFORMED_URL_EXCEPTION, + CDVCommandStatus_IO_EXCEPTION, + CDVCommandStatus_INVALID_ACTION, + CDVCommandStatus_JSON_EXCEPTION, + CDVCommandStatus_ERROR +} CDVCommandStatus; + +@interface CDVPluginResult : NSObject {} + +@property (nonatomic, strong, readonly) NSNumber* status; +@property (nonatomic, strong, readonly) id message; +@property (nonatomic, strong) NSNumber* keepCallback; +// This property can be used to scope the lifetime of another object. For example, +// Use it to store the associated NSData when `message` is created using initWithBytesNoCopy. +@property (nonatomic, strong) id associatedObject; + +- (CDVPluginResult*)init; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArray:(NSArray*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsInt:(int)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSInteger:(NSInteger)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSUInteger:(NSUInteger)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDouble:(double)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsBool:(BOOL)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDictionary:(NSDictionary*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArrayBuffer:(NSData*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsMultipart:(NSArray*)theMessages; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageToErrorObject:(int)errorCode; + ++ (void)setVerbose:(BOOL)verbose; ++ (BOOL)isVerbose; + +- (void)setKeepCallbackAsBool:(BOOL)bKeepCallback; + +- (NSString*)argumentsAsJSON; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVPluginResult.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPluginResult.m new file mode 100755 index 0000000..3521e6d --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVPluginResult.m @@ -0,0 +1,186 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPluginResult.h" +#import "CDVJSON_private.h" +#import "CDVDebug.h" + +@interface CDVPluginResult () + +- (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)theMessage; + +@end + +@implementation CDVPluginResult +@synthesize status, message, keepCallback, associatedObject; + +static NSArray* org_apache_cordova_CommandStatusMsgs; + +id messageFromArrayBuffer(NSData* data) +{ + return @{ + @"CDVType" : @"ArrayBuffer", + @"data" :[data base64EncodedStringWithOptions:0] + }; +} + +id massageMessage(id message) +{ + if ([message isKindOfClass:[NSData class]]) { + return messageFromArrayBuffer(message); + } + return message; +} + +id messageFromMultipart(NSArray* theMessages) +{ + NSMutableArray* messages = [NSMutableArray arrayWithArray:theMessages]; + + for (NSUInteger i = 0; i < messages.count; ++i) { + [messages replaceObjectAtIndex:i withObject:massageMessage([messages objectAtIndex:i])]; + } + + return @{ + @"CDVType" : @"MultiPart", + @"messages" : messages + }; +} + ++ (void)initialize +{ + org_apache_cordova_CommandStatusMsgs = [[NSArray alloc] initWithObjects:@"No result", + @"OK", + @"Class not found", + @"Illegal access", + @"Instantiation error", + @"Malformed url", + @"IO error", + @"Invalid action", + @"JSON error", + @"Error", + nil]; +} + +- (CDVPluginResult*)init +{ + return [self initWithStatus:CDVCommandStatus_NO_RESULT message:nil]; +} + +- (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)theMessage +{ + self = [super init]; + if (self) { + status = [NSNumber numberWithInt:statusOrdinal]; + message = theMessage; + keepCallback = [NSNumber numberWithBool:NO]; + } + return self; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal +{ + return [[self alloc] initWithStatus:statusOrdinal message:nil]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:theMessage]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArray:(NSArray*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:theMessage]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsInt:(int)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithInt:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSInteger:(NSInteger)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithInteger:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsNSUInteger:(NSUInteger)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithUnsignedInteger:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDouble:(double)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithDouble:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsBool:(BOOL)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithBool:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDictionary:(NSDictionary*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:theMessage]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArrayBuffer:(NSData*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:messageFromArrayBuffer(theMessage)]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsMultipart:(NSArray*)theMessages +{ + return [[self alloc] initWithStatus:statusOrdinal message:messageFromMultipart(theMessages)]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageToErrorObject:(int)errorCode +{ + NSDictionary* errDict = @{@"code" :[NSNumber numberWithInt:errorCode]}; + + return [[self alloc] initWithStatus:statusOrdinal message:errDict]; +} + +- (void)setKeepCallbackAsBool:(BOOL)bKeepCallback +{ + [self setKeepCallback:[NSNumber numberWithBool:bKeepCallback]]; +} + +- (NSString*)argumentsAsJSON +{ + id arguments = (self.message == nil ? [NSNull null] : self.message); + NSArray* argumentsWrappedInArray = [NSArray arrayWithObject:arguments]; + + NSString* argumentsJSON = [argumentsWrappedInArray cdv_JSONString]; + + argumentsJSON = [argumentsJSON substringWithRange:NSMakeRange(1, [argumentsJSON length] - 2)]; + + return argumentsJSON; +} + +static BOOL gIsVerbose = NO; ++ (void)setVerbose:(BOOL)verbose +{ + gIsVerbose = verbose; +} + ++ (BOOL)isVerbose +{ + return gIsVerbose; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVScreenOrientationDelegate.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVScreenOrientationDelegate.h new file mode 100755 index 0000000..7226205 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVScreenOrientationDelegate.h @@ -0,0 +1,28 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@protocol CDVScreenOrientationDelegate + +- (NSUInteger)supportedInterfaceOrientations; +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; +- (BOOL)shouldAutorotate; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVTimer.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVTimer.h new file mode 100755 index 0000000..6d31593 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVTimer.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVTimer : NSObject + ++ (void)start:(NSString*)name; ++ (void)stop:(NSString*)name; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVTimer.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVTimer.m new file mode 100755 index 0000000..784e94d --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVTimer.m @@ -0,0 +1,123 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVTimer.h" + +#pragma mark CDVTimerItem + +@interface CDVTimerItem : NSObject + +@property (nonatomic, strong) NSString* name; +@property (nonatomic, strong) NSDate* started; +@property (nonatomic, strong) NSDate* ended; + +- (void)log; + +@end + +@implementation CDVTimerItem + +- (void)log +{ + NSLog(@"[CDVTimer][%@] %fms", self.name, [self.ended timeIntervalSinceDate:self.started] * 1000.0); +} + +@end + +#pragma mark CDVTimer + +@interface CDVTimer () + +@property (nonatomic, strong) NSMutableDictionary* items; + +@end + +@implementation CDVTimer + +#pragma mark object methods + +- (id)init +{ + if (self = [super init]) { + self.items = [NSMutableDictionary dictionaryWithCapacity:6]; + } + + return self; +} + +- (void)add:(NSString*)name +{ + if ([self.items objectForKey:[name lowercaseString]] == nil) { + CDVTimerItem* item = [CDVTimerItem new]; + item.name = name; + item.started = [NSDate new]; + [self.items setObject:item forKey:[name lowercaseString]]; + } else { + NSLog(@"Timer called '%@' already exists.", name); + } +} + +- (void)remove:(NSString*)name +{ + CDVTimerItem* item = [self.items objectForKey:[name lowercaseString]]; + + if (item != nil) { + item.ended = [NSDate new]; + [item log]; + [self.items removeObjectForKey:[name lowercaseString]]; + } else { + NSLog(@"Timer called '%@' does not exist.", name); + } +} + +- (void)removeAll +{ + [self.items removeAllObjects]; +} + +#pragma mark class methods + ++ (void)start:(NSString*)name +{ + [[CDVTimer sharedInstance] add:name]; +} + ++ (void)stop:(NSString*)name +{ + [[CDVTimer sharedInstance] remove:name]; +} + ++ (void)clearAll +{ + [[CDVTimer sharedInstance] removeAll]; +} + ++ (CDVTimer*)sharedInstance +{ + static dispatch_once_t pred = 0; + __strong static CDVTimer* _sharedObject = nil; + + dispatch_once(&pred, ^{ + _sharedObject = [[self alloc] init]; + }); + + return _sharedObject; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVURLProtocol.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVURLProtocol.h new file mode 100755 index 0000000..0561e04 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVURLProtocol.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVAvailability.h" + +@class CDVViewController; + +@interface CDVURLProtocol : NSURLProtocol {} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVURLProtocol.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVURLProtocol.m new file mode 100755 index 0000000..7b2c5ce --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVURLProtocol.m @@ -0,0 +1,113 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import +#import "CDVURLProtocol.h" +#import "CDVCommandQueue.h" +#import "CDVViewController.h" + +// Contains a set of NSNumbers of addresses of controllers. It doesn't store +// the actual pointer to avoid retaining. +static NSMutableSet* gRegisteredControllers = nil; + +NSString* const kCDVAssetsLibraryPrefixes = @"assets-library://"; + +@implementation CDVURLProtocol + + ++ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest +{ + NSURL* theUrl = [theRequest URL]; + + if ([[theUrl absoluteString] hasPrefix:kCDVAssetsLibraryPrefixes]) { + return YES; + } + + return NO; +} + ++ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request +{ + // NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd)); + return request; +} + +- (void)startLoading +{ + // NSLog(@"%@ received %@ - start", self, NSStringFromSelector(_cmd)); + NSURL* url = [[self request] URL]; + + if ([[url absoluteString] hasPrefix:kCDVAssetsLibraryPrefixes]) { + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and send it along. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType); + Byte* buffer = (Byte*)malloc((unsigned long)[assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:(NSUInteger)[assetRepresentation size] error:nil]; + NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + [self sendResponseWithResponseCode:200 data:data mimeType:MIMEType]; + } else { + // Retrieving the asset failed for some reason. Send an error. + [self sendResponseWithResponseCode:404 data:nil mimeType:nil]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send an error. + [self sendResponseWithResponseCode:401 data:nil mimeType:nil]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:url resultBlock:resultBlock failureBlock:failureBlock]; + return; + } + + NSString* body = [NSString stringWithFormat:@"Access not allowed to URL: %@", url]; + [self sendResponseWithResponseCode:401 data:[body dataUsingEncoding:NSASCIIStringEncoding] mimeType:nil]; +} + +- (void)stopLoading +{ + // do any cleanup here +} + ++ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)requestA toRequest:(NSURLRequest*)requestB +{ + return NO; +} + +- (void)sendResponseWithResponseCode:(NSInteger)statusCode data:(NSData*)data mimeType:(NSString*)mimeType +{ + if (mimeType == nil) { + mimeType = @"text/plain"; + } + + NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[[self request] URL] statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : mimeType}]; + + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + if (data != nil) { + [[self client] URLProtocol:self didLoadData:data]; + } + [[self client] URLProtocolDidFinishLoading:self]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVUserAgentUtil.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVUserAgentUtil.h new file mode 100755 index 0000000..4de382f --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVUserAgentUtil.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVUserAgentUtil : NSObject ++ (NSString*)originalUserAgent; ++ (void)acquireLock:(void (^)(NSInteger lockToken))block; ++ (void)releaseLock:(NSInteger*)lockToken; ++ (void)setUserAgent:(NSString*)value lockToken:(NSInteger)lockToken; +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVUserAgentUtil.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVUserAgentUtil.m new file mode 100755 index 0000000..c3402d0 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVUserAgentUtil.m @@ -0,0 +1,122 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVUserAgentUtil.h" + +#import + +// #define VerboseLog NSLog +#define VerboseLog(...) do {} while (0) + +static NSString* const kCdvUserAgentKey = @"Cordova-User-Agent"; +static NSString* const kCdvUserAgentVersionKey = @"Cordova-User-Agent-Version"; + +static NSString* gOriginalUserAgent = nil; +static NSInteger gNextLockToken = 0; +static NSInteger gCurrentLockToken = 0; +static NSMutableArray* gPendingSetUserAgentBlocks = nil; + +@implementation CDVUserAgentUtil + ++ (NSString*)originalUserAgent +{ + if (gOriginalUserAgent == nil) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppLocaleDidChange:) + name:NSCurrentLocaleDidChangeNotification object:nil]; + + NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; + NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; + NSString* localeStr = [[NSLocale currentLocale] localeIdentifier]; + // Record the model since simulator can change it without re-install (CB-5420). + NSString* model = [UIDevice currentDevice].model; + NSString* systemAndLocale = [NSString stringWithFormat:@"%@ %@ %@", model, systemVersion, localeStr]; + + NSString* cordovaUserAgentVersion = [userDefaults stringForKey:kCdvUserAgentVersionKey]; + gOriginalUserAgent = [userDefaults stringForKey:kCdvUserAgentKey]; + BOOL cachedValueIsOld = ![systemAndLocale isEqualToString:cordovaUserAgentVersion]; + + if ((gOriginalUserAgent == nil) || cachedValueIsOld) { + UIWebView* sampleWebView = [[UIWebView alloc] initWithFrame:CGRectZero]; + gOriginalUserAgent = [sampleWebView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; + + [userDefaults setObject:gOriginalUserAgent forKey:kCdvUserAgentKey]; + [userDefaults setObject:systemAndLocale forKey:kCdvUserAgentVersionKey]; + + [userDefaults synchronize]; + } + } + return gOriginalUserAgent; +} + ++ (void)onAppLocaleDidChange:(NSNotification*)notification +{ + // TODO: We should figure out how to update the user-agent of existing UIWebViews when this happens. + // Maybe use the PDF bug (noted in setUserAgent:). + gOriginalUserAgent = nil; +} + ++ (void)acquireLock:(void (^)(NSInteger lockToken))block +{ + if (gCurrentLockToken == 0) { + gCurrentLockToken = ++gNextLockToken; + VerboseLog(@"Gave lock %d", gCurrentLockToken); + block(gCurrentLockToken); + } else { + if (gPendingSetUserAgentBlocks == nil) { + gPendingSetUserAgentBlocks = [[NSMutableArray alloc] initWithCapacity:4]; + } + VerboseLog(@"Waiting for lock"); + [gPendingSetUserAgentBlocks addObject:block]; + } +} + ++ (void)releaseLock:(NSInteger*)lockToken +{ + if (*lockToken == 0) { + return; + } + NSAssert(gCurrentLockToken == *lockToken, @"Got token %ld, expected %ld", (long)*lockToken, (long)gCurrentLockToken); + + VerboseLog(@"Released lock %d", *lockToken); + if ([gPendingSetUserAgentBlocks count] > 0) { + void (^block)() = [gPendingSetUserAgentBlocks objectAtIndex:0]; + [gPendingSetUserAgentBlocks removeObjectAtIndex:0]; + gCurrentLockToken = ++gNextLockToken; + NSLog(@"Gave lock %ld", (long)gCurrentLockToken); + block(gCurrentLockToken); + } else { + gCurrentLockToken = 0; + } + *lockToken = 0; +} + ++ (void)setUserAgent:(NSString*)value lockToken:(NSInteger)lockToken +{ + NSAssert(gCurrentLockToken == lockToken, @"Got token %ld, expected %ld", (long)lockToken, (long)gCurrentLockToken); + VerboseLog(@"User-Agent set to: %@", value); + + // Setting the UserAgent must occur before a UIWebView is instantiated. + // It is read per instantiation, so it does not affect previously created views. + // Except! When a PDF is loaded, all currently active UIWebViews reload their + // User-Agent from the NSUserDefaults some time after the DidFinishLoad of the PDF bah! + NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:value, @"UserAgent", nil]; + [[NSUserDefaults standardUserDefaults] registerDefaults:dict]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVViewController.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVViewController.h new file mode 100755 index 0000000..a8a35d3 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVViewController.h @@ -0,0 +1,85 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import "CDVAvailability.h" +#import "CDVInvokedUrlCommand.h" +#import "CDVCommandDelegate.h" +#import "CDVCommandQueue.h" +#import "CDVScreenOrientationDelegate.h" +#import "CDVPlugin.h" +#import "CDVWebViewEngineProtocol.h" + +@interface CDVViewController : UIViewController { + @protected + id _webViewEngine; + @protected + id _commandDelegate; + @protected + CDVCommandQueue* _commandQueue; + NSString* _userAgent; +} + +@property (nonatomic, readonly, weak) IBOutlet UIView* webView; + +@property (nonatomic, readonly, strong) NSMutableDictionary* pluginObjects; +@property (nonatomic, readonly, strong) NSDictionary* pluginsMap; +@property (nonatomic, readonly, strong) NSMutableDictionary* settings; +@property (nonatomic, readonly, strong) NSXMLParser* configParser; + +@property (nonatomic, readwrite, copy) NSString* configFile; +@property (nonatomic, readwrite, copy) NSString* wwwFolderName; +@property (nonatomic, readwrite, copy) NSString* startPage; +@property (nonatomic, readonly, strong) CDVCommandQueue* commandQueue; +@property (nonatomic, readonly, strong) id webViewEngine; +@property (nonatomic, readonly, strong) id commandDelegate; + +/** + The complete user agent that Cordova will use when sending web requests. + */ +@property (nonatomic, readonly) NSString* userAgent; + +/** + The base user agent data that Cordova will use to build its user agent. If this + property isn't set, Cordova will use the standard web view user agent as its + base. + */ +@property (nonatomic, readwrite, copy) NSString* baseUserAgent; + +/** + The address of the lock token used for controlling access to setting the user-agent + */ +@property (nonatomic, readonly) NSInteger* userAgentLockToken; + +- (UIView*)newCordovaViewWithFrame:(CGRect)bounds; + +- (NSString*)appURLScheme; +- (NSURL*)errorURL; + +- (NSArray*)parseInterfaceOrientations:(NSArray*)orientations; +- (BOOL)supportsOrientation:(UIInterfaceOrientation)orientation; + +- (id)getCommandInstance:(NSString*)pluginName; +- (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className; +- (void)registerPlugin:(CDVPlugin*)plugin withPluginName:(NSString*)pluginName; + +- (void)parseSettingsWithParser:(NSObject *)delegate; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVViewController.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVViewController.m new file mode 100755 index 0000000..000d184 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVViewController.m @@ -0,0 +1,671 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDV.h" +#import "CDVPlugin+Private.h" +#import "CDVUIWebViewDelegate.h" +#import "CDVConfigParser.h" +#import "CDVUserAgentUtil.h" +#import +#import "NSDictionary+CordovaPreferences.h" +#import "CDVLocalStorage.h" +#import "CDVCommandDelegateImpl.h" + +@interface CDVViewController () { + NSInteger _userAgentLockToken; +} + +@property (nonatomic, readwrite, strong) NSXMLParser* configParser; +@property (nonatomic, readwrite, strong) NSMutableDictionary* settings; +@property (nonatomic, readwrite, strong) NSMutableDictionary* pluginObjects; +@property (nonatomic, readwrite, strong) NSMutableArray* startupPluginNames; +@property (nonatomic, readwrite, strong) NSDictionary* pluginsMap; +@property (nonatomic, readwrite, strong) NSArray* supportedOrientations; +@property (nonatomic, readwrite, strong) id webViewEngine; + +@property (readwrite, assign) BOOL initialized; + +@property (atomic, strong) NSURL* openURL; + +@end + +@implementation CDVViewController + +@synthesize supportedOrientations; +@synthesize pluginObjects, pluginsMap, startupPluginNames; +@synthesize configParser, settings; +@synthesize wwwFolderName, startPage, initialized, openURL, baseUserAgent; +@synthesize commandDelegate = _commandDelegate; +@synthesize commandQueue = _commandQueue; +@synthesize webViewEngine = _webViewEngine; +@dynamic webView; + +- (void)__init +{ + if ((self != nil) && !self.initialized) { + _commandQueue = [[CDVCommandQueue alloc] initWithViewController:self]; + _commandDelegate = [[CDVCommandDelegateImpl alloc] initWithViewController:self]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillTerminate:) + name:UIApplicationWillTerminateNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillResignActive:) + name:UIApplicationWillResignActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification object:nil]; + + // read from UISupportedInterfaceOrientations (or UISupportedInterfaceOrientations~iPad, if its iPad) from -Info.plist + self.supportedOrientations = [self parseInterfaceOrientations: + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UISupportedInterfaceOrientations"]]; + + [self printVersion]; + [self printMultitaskingInfo]; + [self printPlatformVersionWarning]; + self.initialized = YES; + } +} + +- (id)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + [self __init]; + return self; +} + +- (id)initWithCoder:(NSCoder*)aDecoder +{ + self = [super initWithCoder:aDecoder]; + [self __init]; + return self; +} + +- (id)init +{ + self = [super init]; + [self __init]; + return self; +} + +- (void)printVersion +{ + NSLog(@"Apache Cordova native platform version %@ is starting.", CDV_VERSION); +} + +- (void)printPlatformVersionWarning +{ + if (!IsAtLeastiOSVersion(@"8.0")) { + NSLog(@"CRITICAL: For Cordova 4.0.0 and above, you will need to upgrade to at least iOS 8.0 or greater. Your current version of iOS is %@.", + [[UIDevice currentDevice] systemVersion] + ); + } +} + +- (void)printMultitaskingInfo +{ + UIDevice* device = [UIDevice currentDevice]; + BOOL backgroundSupported = NO; + + if ([device respondsToSelector:@selector(isMultitaskingSupported)]) { + backgroundSupported = device.multitaskingSupported; + } + + NSNumber* exitsOnSuspend = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationExitsOnSuspend"]; + if (exitsOnSuspend == nil) { // if it's missing, it should be NO (i.e. multi-tasking on by default) + exitsOnSuspend = [NSNumber numberWithBool:NO]; + } + + NSLog(@"Multi-tasking -> Device: %@, App: %@", (backgroundSupported ? @"YES" : @"NO"), (![exitsOnSuspend intValue]) ? @"YES" : @"NO"); +} + +-(NSString*)configFilePath{ + NSString* path = self.configFile ?: @"config.xml"; + + // if path is relative, resolve it against the main bundle + if(![path isAbsolutePath]){ + NSString* absolutePath = [[NSBundle mainBundle] pathForResource:path ofType:nil]; + if(!absolutePath){ + NSAssert(NO, @"ERROR: %@ not found in the main bundle!", path); + } + path = absolutePath; + } + + // Assert file exists + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSAssert(NO, @"ERROR: %@ does not exist. Please run cordova-ios/bin/cordova_plist_to_config_xml path/to/project.", path); + return nil; + } + + return path; +} + +- (void)parseSettingsWithParser:(NSObject *)delegate +{ + // read from config.xml in the app bundle + NSString* path = [self configFilePath]; + + NSURL* url = [NSURL fileURLWithPath:path]; + + self.configParser = [[NSXMLParser alloc] initWithContentsOfURL:url]; + if (self.configParser == nil) { + NSLog(@"Failed to initialize XML parser."); + return; + } + [self.configParser setDelegate:((id < NSXMLParserDelegate >)delegate)]; + [self.configParser parse]; +} + +- (void)loadSettings +{ + CDVConfigParser* delegate = [[CDVConfigParser alloc] init]; + + [self parseSettingsWithParser:delegate]; + + // Get the plugin dictionary, whitelist and settings from the delegate. + self.pluginsMap = delegate.pluginsDict; + self.startupPluginNames = delegate.startupPluginNames; + self.settings = delegate.settings; + + // And the start folder/page. + if(self.wwwFolderName == nil){ + self.wwwFolderName = @"www"; + } + if(delegate.startPage && self.startPage == nil){ + self.startPage = delegate.startPage; + } + if (self.startPage == nil) { + self.startPage = @"index.html"; + } + + // Initialize the plugin objects dict. + self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20]; +} + +- (NSURL*)appUrl +{ + NSURL* appURL = nil; + + if ([self.startPage rangeOfString:@"://"].location != NSNotFound) { + appURL = [NSURL URLWithString:self.startPage]; + } else if ([self.wwwFolderName rangeOfString:@"://"].location != NSNotFound) { + appURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", self.wwwFolderName, self.startPage]]; + } else if([self.wwwFolderName hasSuffix:@".bundle"]){ + // www folder is actually a bundle + NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName]; + appURL = [bundle URLForResource:self.startPage withExtension:nil]; + } else { + // CB-3005 strip parameters from start page to check if page exists in resources + NSURL* startURL = [NSURL URLWithString:self.startPage]; + NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]]; + + if (startFilePath == nil) { + appURL = nil; + } else { + appURL = [NSURL fileURLWithPath:startFilePath]; + // CB-3005 Add on the query params or fragment. + NSString* startPageNoParentDirs = self.startPage; + NSRange r = [startPageNoParentDirs rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"?#"] options:0]; + if (r.location != NSNotFound) { + NSString* queryAndOrFragment = [self.startPage substringFromIndex:r.location]; + appURL = [NSURL URLWithString:queryAndOrFragment relativeToURL:appURL]; + } + } + } + + return appURL; +} + +- (NSURL*)errorURL +{ + NSURL* errorUrl = nil; + + id setting = [self.settings cordovaSettingForKey:@"ErrorUrl"]; + + if (setting) { + NSString* errorUrlString = (NSString*)setting; + if ([errorUrlString rangeOfString:@"://"].location != NSNotFound) { + errorUrl = [NSURL URLWithString:errorUrlString]; + } else { + NSURL* url = [NSURL URLWithString:(NSString*)setting]; + NSString* errorFilePath = [self.commandDelegate pathForResource:[url path]]; + if (errorFilePath) { + errorUrl = [NSURL fileURLWithPath:errorFilePath]; + } + } + } + + return errorUrl; +} + +- (UIView*)webView +{ + if (self.webViewEngine != nil) { + return self.webViewEngine.engineWebView; + } + + return nil; +} + +// Implement viewDidLoad to do additional setup after loading the view, typically from a nib. +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // Load settings + [self loadSettings]; + + NSString* backupWebStorageType = @"cloud"; // default value + + id backupWebStorage = [self.settings cordovaSettingForKey:@"BackupWebStorage"]; + if ([backupWebStorage isKindOfClass:[NSString class]]) { + backupWebStorageType = backupWebStorage; + } + [self.settings setCordovaSetting:backupWebStorageType forKey:@"BackupWebStorage"]; + + [CDVLocalStorage __fixupDatabaseLocationsWithBackupType:backupWebStorageType]; + + // // Instantiate the WebView /////////////// + + if (!self.webView) { + [self createGapView]; + } + + // ///////////////// + + /* + * Fire up CDVLocalStorage to work-around WebKit storage limitations: on all iOS 5.1+ versions for local-only backups, but only needed on iOS 5.1 for cloud backup. + With minimum iOS 7/8 supported, only first clause applies. + */ + if ([backupWebStorageType isEqualToString:@"local"]) { + NSString* localStorageFeatureName = @"localstorage"; + if ([self.pluginsMap objectForKey:localStorageFeatureName]) { // plugin specified in config + [self.startupPluginNames addObject:localStorageFeatureName]; + } + } + + if ([self.startupPluginNames count] > 0) { + [CDVTimer start:@"TotalPluginStartup"]; + + for (NSString* pluginName in self.startupPluginNames) { + [CDVTimer start:pluginName]; + [self getCommandInstance:pluginName]; + [CDVTimer stop:pluginName]; + } + + [CDVTimer stop:@"TotalPluginStartup"]; + } + + // ///////////////// + NSURL* appURL = [self appUrl]; + + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:self.userAgent lockToken:lockToken]; + if (appURL) { + NSURLRequest* appReq = [NSURLRequest requestWithURL:appURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0]; + [self.webViewEngine loadRequest:appReq]; + } else { + NSString* loadErr = [NSString stringWithFormat:@"ERROR: Start Page at '%@/%@' was not found.", self.wwwFolderName, self.startPage]; + NSLog(@"%@", loadErr); + + NSURL* errorUrl = [self errorURL]; + if (errorUrl) { + errorUrl = [NSURL URLWithString:[NSString stringWithFormat:@"?error=%@", [loadErr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] relativeToURL:errorUrl]; + NSLog(@"%@", [errorUrl absoluteString]); + [self.webViewEngine loadRequest:[NSURLRequest requestWithURL:errorUrl]]; + } else { + NSString* html = [NSString stringWithFormat:@" %@ ", loadErr]; + [self.webViewEngine loadHTMLString:html baseURL:nil]; + } + } + }]; +} + +- (NSArray*)parseInterfaceOrientations:(NSArray*)orientations +{ + NSMutableArray* result = [[NSMutableArray alloc] init]; + + if (orientations != nil) { + NSEnumerator* enumerator = [orientations objectEnumerator]; + NSString* orientationString; + + while (orientationString = [enumerator nextObject]) { + if ([orientationString isEqualToString:@"UIInterfaceOrientationPortrait"]) { + [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationPortrait]]; + } else if ([orientationString isEqualToString:@"UIInterfaceOrientationPortraitUpsideDown"]) { + [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationPortraitUpsideDown]]; + } else if ([orientationString isEqualToString:@"UIInterfaceOrientationLandscapeLeft"]) { + [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationLandscapeLeft]]; + } else if ([orientationString isEqualToString:@"UIInterfaceOrientationLandscapeRight"]) { + [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight]]; + } + } + } + + // default + if ([result count] == 0) { + [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationPortrait]]; + } + + return result; +} + +- (BOOL)shouldAutorotate +{ + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + NSUInteger ret = 0; + + if ([self supportsOrientation:UIInterfaceOrientationPortrait]) { + ret = ret | (1 << UIInterfaceOrientationPortrait); + } + if ([self supportsOrientation:UIInterfaceOrientationPortraitUpsideDown]) { + ret = ret | (1 << UIInterfaceOrientationPortraitUpsideDown); + } + if ([self supportsOrientation:UIInterfaceOrientationLandscapeRight]) { + ret = ret | (1 << UIInterfaceOrientationLandscapeRight); + } + if ([self supportsOrientation:UIInterfaceOrientationLandscapeLeft]) { + ret = ret | (1 << UIInterfaceOrientationLandscapeLeft); + } + + return ret; +} + +- (BOOL)supportsOrientation:(UIInterfaceOrientation)orientation +{ + return [self.supportedOrientations containsObject:[NSNumber numberWithInt:orientation]]; +} + +- (UIView*)newCordovaViewWithFrame:(CGRect)bounds +{ + NSString* defaultWebViewEngineClass = @"CDVUIWebViewEngine"; + NSString* webViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaWebViewEngine"]; + + if (!webViewEngineClass) { + webViewEngineClass = defaultWebViewEngineClass; + } + + // Find webViewEngine + if (NSClassFromString(webViewEngineClass)) { + self.webViewEngine = [[NSClassFromString(webViewEngineClass) alloc] initWithFrame:bounds]; + // if a webView engine returns nil (not supported by the current iOS version) or doesn't conform to the protocol, or can't load the request, we use UIWebView + if (!self.webViewEngine || ![self.webViewEngine conformsToProtocol:@protocol(CDVWebViewEngineProtocol)] || ![self.webViewEngine canLoadRequest:[NSURLRequest requestWithURL:self.appUrl]]) { + self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds]; + } + } else { + self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds]; + } + + if ([self.webViewEngine isKindOfClass:[CDVPlugin class]]) { + [self registerPlugin:(CDVPlugin*)self.webViewEngine withClassName:webViewEngineClass]; + } + + return self.webViewEngine.engineWebView; +} + +- (NSString*)userAgent +{ + if (_userAgent != nil) { + return _userAgent; + } + + NSString* localBaseUserAgent; + if (self.baseUserAgent != nil) { + localBaseUserAgent = self.baseUserAgent; + } else if ([self.settings cordovaSettingForKey:@"OverrideUserAgent"] != nil) { + localBaseUserAgent = [self.settings cordovaSettingForKey:@"OverrideUserAgent"]; + } else { + localBaseUserAgent = [CDVUserAgentUtil originalUserAgent]; + } + NSString* appendUserAgent = [self.settings cordovaSettingForKey:@"AppendUserAgent"]; + if (appendUserAgent) { + _userAgent = [NSString stringWithFormat:@"%@ %@", localBaseUserAgent, appendUserAgent]; + } else { + // Use our address as a unique number to append to the User-Agent. + _userAgent = [NSString stringWithFormat:@"%@ (%lld)", localBaseUserAgent, (long long)self]; + } + return _userAgent; +} + +- (void)createGapView +{ + CGRect webViewBounds = self.view.bounds; + + webViewBounds.origin = self.view.bounds.origin; + + UIView* view = [self newCordovaViewWithFrame:webViewBounds]; + + view.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + [self.view addSubview:view]; + [self.view sendSubviewToBack:view]; +} + +- (void)didReceiveMemoryWarning +{ + // iterate through all the plugin objects, and call hasPendingOperation + // if at least one has a pending operation, we don't call [super didReceiveMemoryWarning] + + NSEnumerator* enumerator = [self.pluginObjects objectEnumerator]; + CDVPlugin* plugin; + + BOOL doPurge = YES; + + while ((plugin = [enumerator nextObject])) { + if (plugin.hasPendingOperation) { + NSLog(@"Plugin '%@' has a pending operation, memory purge is delayed for didReceiveMemoryWarning.", NSStringFromClass([plugin class])); + doPurge = NO; + } + } + + if (doPurge) { + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + } + + // Release any cached data, images, etc. that aren't in use. +} + +- (void)viewDidUnload +{ + // Release any retained subviews of the main view. + // e.g. self.myOutlet = nil; + + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + + [super viewDidUnload]; +} + +#pragma mark CordovaCommands + +- (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className +{ + if ([plugin respondsToSelector:@selector(setViewController:)]) { + [plugin setViewController:self]; + } + + if ([plugin respondsToSelector:@selector(setCommandDelegate:)]) { + [plugin setCommandDelegate:_commandDelegate]; + } + + [self.pluginObjects setObject:plugin forKey:className]; + [plugin pluginInitialize]; +} + +- (void)registerPlugin:(CDVPlugin*)plugin withPluginName:(NSString*)pluginName +{ + if ([plugin respondsToSelector:@selector(setViewController:)]) { + [plugin setViewController:self]; + } + + if ([plugin respondsToSelector:@selector(setCommandDelegate:)]) { + [plugin setCommandDelegate:_commandDelegate]; + } + + NSString* className = NSStringFromClass([plugin class]); + [self.pluginObjects setObject:plugin forKey:className]; + [self.pluginsMap setValue:className forKey:[pluginName lowercaseString]]; + [plugin pluginInitialize]; +} + +/** + Returns an instance of a CordovaCommand object, based on its name. If one exists already, it is returned. + */ +- (id)getCommandInstance:(NSString*)pluginName +{ + // first, we try to find the pluginName in the pluginsMap + // (acts as a whitelist as well) if it does not exist, we return nil + // NOTE: plugin names are matched as lowercase to avoid problems - however, a + // possible issue is there can be duplicates possible if you had: + // "org.apache.cordova.Foo" and "org.apache.cordova.foo" - only the lower-cased entry will match + NSString* className = [self.pluginsMap objectForKey:[pluginName lowercaseString]]; + + if (className == nil) { + return nil; + } + + id obj = [self.pluginObjects objectForKey:className]; + if (!obj) { + obj = [[NSClassFromString(className)alloc] initWithWebViewEngine:_webViewEngine]; + + if (obj != nil) { + [self registerPlugin:obj withClassName:className]; + } else { + NSLog(@"CDVPlugin class %@ (pluginName: %@) does not exist.", className, pluginName); + } + } + return obj; +} + +#pragma mark - + +- (NSString*)appURLScheme +{ + NSString* URLScheme = nil; + + NSArray* URLTypes = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleURLTypes"]; + + if (URLTypes != nil) { + NSDictionary* dict = [URLTypes objectAtIndex:0]; + if (dict != nil) { + NSArray* URLSchemes = [dict objectForKey:@"CFBundleURLSchemes"]; + if (URLSchemes != nil) { + URLScheme = [URLSchemes objectAtIndex:0]; + } + } + } + + return URLScheme; +} + +#pragma mark - +#pragma mark UIApplicationDelegate impl + +/* + This method lets your application know that it is about to be terminated and purged from memory entirely + */ +- (void)onAppWillTerminate:(NSNotification*)notification +{ + // empty the tmp directory + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSError* __autoreleasing err = nil; + + // clear contents of NSTemporaryDirectory + NSString* tempDirectoryPath = NSTemporaryDirectory(); + NSDirectoryEnumerator* directoryEnumerator = [fileMgr enumeratorAtPath:tempDirectoryPath]; + NSString* fileName = nil; + BOOL result; + + while ((fileName = [directoryEnumerator nextObject])) { + NSString* filePath = [tempDirectoryPath stringByAppendingPathComponent:fileName]; + result = [fileMgr removeItemAtPath:filePath error:&err]; + if (!result && err) { + NSLog(@"Failed to delete: %@ (error: %@)", filePath, err); + } + } +} + +/* + This method is called to let your application know that it is about to move from the active to inactive state. + You should use this method to pause ongoing tasks, disable timer, ... + */ +- (void)onAppWillResignActive:(NSNotification*)notification +{ + // NSLog(@"%@",@"applicationWillResignActive"); + [self.commandDelegate evalJs:@"cordova.fireDocumentEvent('resign');" scheduledOnRunLoop:NO]; +} + +/* + In iOS 4.0 and later, this method is called as part of the transition from the background to the inactive state. + You can use this method to undo many of the changes you made to your application upon entering the background. + invariably followed by applicationDidBecomeActive + */ +- (void)onAppWillEnterForeground:(NSNotification*)notification +{ + // NSLog(@"%@",@"applicationWillEnterForeground"); + [self.commandDelegate evalJs:@"cordova.fireDocumentEvent('resume');"]; + + /** Clipboard fix **/ + UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; + NSString* string = pasteboard.string; + if (string) { + [pasteboard setValue:string forPasteboardType:@"public.text"]; + } +} + +// This method is called to let your application know that it moved from the inactive to active state. +- (void)onAppDidBecomeActive:(NSNotification*)notification +{ + // NSLog(@"%@",@"applicationDidBecomeActive"); + [self.commandDelegate evalJs:@"cordova.fireDocumentEvent('active');"]; +} + +/* + In iOS 4.0 and later, this method is called instead of the applicationWillTerminate: method + when the user quits an application that supports background execution. + */ +- (void)onAppDidEnterBackground:(NSNotification*)notification +{ + // NSLog(@"%@",@"applicationDidEnterBackground"); + [self.commandDelegate evalJs:@"cordova.fireDocumentEvent('pause', null, true);" scheduledOnRunLoop:NO]; +} + +// /////////////////////// + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + [_commandQueue dispose]; + [[self.pluginObjects allValues] makeObjectsPerformSelector:@selector(dispose)]; +} + +- (NSInteger*)userAgentLockToken +{ + return &_userAgentLockToken; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h new file mode 100755 index 0000000..34d07f3 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h @@ -0,0 +1,42 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +#define kCDVWebViewEngineScriptMessageHandlers @"kCDVWebViewEngineScriptMessageHandlers" +#define kCDVWebViewEngineUIWebViewDelegate @"kCDVWebViewEngineUIWebViewDelegate" +#define kCDVWebViewEngineWKNavigationDelegate @"kCDVWebViewEngineWKNavigationDelegate" +#define kCDVWebViewEngineWKUIDelegate @"kCDVWebViewEngineWKUIDelegate" +#define kCDVWebViewEngineWebViewPreferences @"kCDVWebViewEngineWebViewPreferences" + +@protocol CDVWebViewEngineProtocol + +@property (nonatomic, strong, readonly) UIView* engineWebView; + +- (id)loadRequest:(NSURLRequest*)request; +- (id)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL; +- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void (^)(id, NSError*))completionHandler; + +- (NSURL*)URL; +- (BOOL)canLoadRequest:(NSURLRequest*)request; + +- (instancetype)initWithFrame:(CGRect)frame; +- (void)updateWithInfo:(NSDictionary*)info; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVWhitelist.h b/msext.xcodeproj/CordovaLib/Classes/Public/CDVWhitelist.h new file mode 100755 index 0000000..9165097 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVWhitelist.h @@ -0,0 +1,34 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +extern NSString* const kCDVDefaultWhitelistRejectionString; + +@interface CDVWhitelist : NSObject + +@property (nonatomic, copy) NSString* whitelistRejectionFormatString; + +- (id)initWithArray:(NSArray*)array; +- (BOOL)schemeIsAllowed:(NSString*)scheme; +- (BOOL)URLIsAllowed:(NSURL*)url; +- (BOOL)URLIsAllowed:(NSURL*)url logFailure:(BOOL)logFailure; +- (NSString*)errorStringForURL:(NSURL*)url; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/CDVWhitelist.m b/msext.xcodeproj/CordovaLib/Classes/Public/CDVWhitelist.m new file mode 100755 index 0000000..552ea95 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/CDVWhitelist.m @@ -0,0 +1,285 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWhitelist.h" + +NSString* const kCDVDefaultWhitelistRejectionString = @"ERROR whitelist rejection: url='%@'"; +NSString* const kCDVDefaultSchemeName = @"cdv-default-scheme"; + +@interface CDVWhitelistPattern : NSObject { + @private + NSRegularExpression* _scheme; + NSRegularExpression* _host; + NSNumber* _port; + NSRegularExpression* _path; +} + ++ (NSString*)regexFromPattern:(NSString*)pattern allowWildcards:(bool)allowWildcards; +- (id)initWithScheme:(NSString*)scheme host:(NSString*)host port:(NSString*)port path:(NSString*)path; +- (bool)matches:(NSURL*)url; + +@end + +@implementation CDVWhitelistPattern + ++ (NSString*)regexFromPattern:(NSString*)pattern allowWildcards:(bool)allowWildcards +{ + NSString* regex = [NSRegularExpression escapedPatternForString:pattern]; + + if (allowWildcards) { + regex = [regex stringByReplacingOccurrencesOfString:@"\\*" withString:@".*"]; + + /* [NSURL path] has the peculiarity that a trailing slash at the end of a path + * will be omitted. This regex tweak compensates for that. + */ + if ([regex hasSuffix:@"\\/.*"]) { + regex = [NSString stringWithFormat:@"%@(\\/.*)?", [regex substringToIndex:([regex length] - 4)]]; + } + } + return [NSString stringWithFormat:@"%@$", regex]; +} + +- (id)initWithScheme:(NSString*)scheme host:(NSString*)host port:(NSString*)port path:(NSString*)path +{ + self = [super init]; // Potentially change "self" + if (self) { + if ((scheme == nil) || [scheme isEqualToString:@"*"]) { + _scheme = nil; + } else { + _scheme = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:scheme allowWildcards:NO] options:NSRegularExpressionCaseInsensitive error:nil]; + } + if ([host isEqualToString:@"*"] || host == nil) { + _host = nil; + } else if ([host hasPrefix:@"*."]) { + _host = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"([a-z0-9.-]*\\.)?%@", [CDVWhitelistPattern regexFromPattern:[host substringFromIndex:2] allowWildcards:false]] options:NSRegularExpressionCaseInsensitive error:nil]; + } else { + _host = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:host allowWildcards:NO] options:NSRegularExpressionCaseInsensitive error:nil]; + } + if ((port == nil) || [port isEqualToString:@"*"]) { + _port = nil; + } else { + _port = [[NSNumber alloc] initWithInteger:[port integerValue]]; + } + if ((path == nil) || [path isEqualToString:@"/*"]) { + _path = nil; + } else { + _path = [NSRegularExpression regularExpressionWithPattern:[CDVWhitelistPattern regexFromPattern:path allowWildcards:YES] options:0 error:nil]; + } + } + return self; +} + +- (bool)matches:(NSURL*)url +{ + return (_scheme == nil || [_scheme numberOfMatchesInString:[url scheme] options:NSMatchingAnchored range:NSMakeRange(0, [[url scheme] length])]) && + (_host == nil || ([url host] != nil && [_host numberOfMatchesInString:[url host] options:NSMatchingAnchored range:NSMakeRange(0, [[url host] length])])) && + (_port == nil || [[url port] isEqualToNumber:_port]) && + (_path == nil || [_path numberOfMatchesInString:[url path] options:NSMatchingAnchored range:NSMakeRange(0, [[url path] length])]) + ; +} + +@end + +@interface CDVWhitelist () + +@property (nonatomic, readwrite, strong) NSMutableArray* whitelist; +@property (nonatomic, readwrite, strong) NSMutableSet* permittedSchemes; + +- (void)addWhiteListEntry:(NSString*)pattern; + +@end + +@implementation CDVWhitelist + +@synthesize whitelist, permittedSchemes, whitelistRejectionFormatString; + +- (id)initWithArray:(NSArray*)array +{ + self = [super init]; + if (self) { + self.whitelist = [[NSMutableArray alloc] init]; + self.permittedSchemes = [[NSMutableSet alloc] init]; + self.whitelistRejectionFormatString = kCDVDefaultWhitelistRejectionString; + + for (NSString* pattern in array) { + [self addWhiteListEntry:pattern]; + } + } + return self; +} + +- (BOOL)isIPv4Address:(NSString*)externalHost +{ + // an IPv4 address has 4 octets b.b.b.b where b is a number between 0 and 255. + // for our purposes, b can also be the wildcard character '*' + + // we could use a regex to solve this problem but then I would have two problems + // anyways, this is much clearer and maintainable + NSArray* octets = [externalHost componentsSeparatedByString:@"."]; + NSUInteger num_octets = [octets count]; + + // quick check + if (num_octets != 4) { + return NO; + } + + // restrict number parsing to 0-255 + NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setMinimum:[NSNumber numberWithUnsignedInteger:0]]; + [numberFormatter setMaximum:[NSNumber numberWithUnsignedInteger:255]]; + + // iterate through each octet, and test for a number between 0-255 or if it equals '*' + for (NSUInteger i = 0; i < num_octets; ++i) { + NSString* octet = [octets objectAtIndex:i]; + + if ([octet isEqualToString:@"*"]) { // passes - check next octet + continue; + } else if ([numberFormatter numberFromString:octet] == nil) { // fails - not a number and not within our range, return + return NO; + } + } + + return YES; +} + +- (void)addWhiteListEntry:(NSString*)origin +{ + if (self.whitelist == nil) { + return; + } + + if ([origin isEqualToString:@"*"]) { + NSLog(@"Unlimited access to network resources"); + self.whitelist = nil; + self.permittedSchemes = nil; + } else { // specific access + NSRegularExpression* parts = [NSRegularExpression regularExpressionWithPattern:@"^((\\*|[A-Za-z-]+):/?/?)?(((\\*\\.)?[^*/:]+)|\\*)?(:(\\d+))?(/.*)?" options:0 error:nil]; + NSTextCheckingResult* m = [parts firstMatchInString:origin options:NSMatchingAnchored range:NSMakeRange(0, [origin length])]; + if (m != nil) { + NSRange r; + NSString* scheme = nil; + r = [m rangeAtIndex:2]; + if (r.location != NSNotFound) { + scheme = [origin substringWithRange:r]; + } + + NSString* host = nil; + r = [m rangeAtIndex:3]; + if (r.location != NSNotFound) { + host = [origin substringWithRange:r]; + } + + // Special case for two urls which are allowed to have empty hosts + if (([scheme isEqualToString:@"file"] || [scheme isEqualToString:@"content"]) && (host == nil)) { + host = @"*"; + } + + NSString* port = nil; + r = [m rangeAtIndex:7]; + if (r.location != NSNotFound) { + port = [origin substringWithRange:r]; + } + + NSString* path = nil; + r = [m rangeAtIndex:8]; + if (r.location != NSNotFound) { + path = [origin substringWithRange:r]; + } + + if (scheme == nil) { + // XXX making it stupid friendly for people who forget to include protocol/SSL + [self.whitelist addObject:[[CDVWhitelistPattern alloc] initWithScheme:@"http" host:host port:port path:path]]; + [self.whitelist addObject:[[CDVWhitelistPattern alloc] initWithScheme:@"https" host:host port:port path:path]]; + } else { + [self.whitelist addObject:[[CDVWhitelistPattern alloc] initWithScheme:scheme host:host port:port path:path]]; + } + + if (self.permittedSchemes != nil) { + if ([scheme isEqualToString:@"*"]) { + self.permittedSchemes = nil; + } else if (scheme != nil) { + [self.permittedSchemes addObject:scheme]; + } + } + } + } +} + +- (BOOL)schemeIsAllowed:(NSString*)scheme +{ + if ([scheme isEqualToString:@"http"] || + [scheme isEqualToString:@"https"] || + [scheme isEqualToString:@"ftp"] || + [scheme isEqualToString:@"ftps"]) { + return YES; + } + + return (self.permittedSchemes == nil) || [self.permittedSchemes containsObject:scheme]; +} + +- (BOOL)URLIsAllowed:(NSURL*)url +{ + return [self URLIsAllowed:url logFailure:YES]; +} + +- (BOOL)URLIsAllowed:(NSURL*)url logFailure:(BOOL)logFailure +{ + // Shortcut acceptance: Are all urls whitelisted ("*" in whitelist)? + if (whitelist == nil) { + return YES; + } + + // Shortcut rejection: Check that the scheme is supported + NSString* scheme = [[url scheme] lowercaseString]; + if (![self schemeIsAllowed:scheme]) { + if (logFailure) { + NSLog(@"%@", [self errorStringForURL:url]); + } + return NO; + } + + // http[s] and ftp[s] should also validate against the common set in the kCDVDefaultSchemeName list + if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"] || [scheme isEqualToString:@"ftp"] || [scheme isEqualToString:@"ftps"]) { + NSURL* newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@%@", kCDVDefaultSchemeName, [url host], [[url path] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; + // If it is allowed, we are done. If not, continue to check for the actual scheme-specific list + if ([self URLIsAllowed:newUrl logFailure:NO]) { + return YES; + } + } + + // Check the url against patterns in the whitelist + for (CDVWhitelistPattern* p in self.whitelist) { + if ([p matches:url]) { + return YES; + } + } + + if (logFailure) { + NSLog(@"%@", [self errorStringForURL:url]); + } + // if we got here, the url host is not in the white-list, do nothing + return NO; +} + +- (NSString*)errorStringForURL:(NSURL*)url +{ + return [NSString stringWithFormat:self.whitelistRejectionFormatString, [url absoluteString]]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/NSDictionary+CordovaPreferences.h b/msext.xcodeproj/CordovaLib/Classes/Public/NSDictionary+CordovaPreferences.h new file mode 100755 index 0000000..9be2be2 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/NSDictionary+CordovaPreferences.h @@ -0,0 +1,35 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import + +@interface NSDictionary (CordovaPreferences) + +- (id)cordovaSettingForKey:(NSString*)key; +- (BOOL)cordovaBoolSettingForKey:(NSString*)key defaultValue:(BOOL)defaultValue; +- (CGFloat)cordovaFloatSettingForKey:(NSString*)key defaultValue:(CGFloat)defaultValue; + +@end + +@interface NSMutableDictionary (CordovaPreferences) + +- (void)setCordovaSetting:(id)value forKey:(NSString*)key; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/NSDictionary+CordovaPreferences.m b/msext.xcodeproj/CordovaLib/Classes/Public/NSDictionary+CordovaPreferences.m new file mode 100755 index 0000000..dcac40f --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/NSDictionary+CordovaPreferences.m @@ -0,0 +1,63 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "NSDictionary+CordovaPreferences.h" +#import + +@implementation NSDictionary (CordovaPreferences) + +- (id)cordovaSettingForKey:(NSString*)key +{ + return [self objectForKey:[key lowercaseString]]; +} + +- (BOOL)cordovaBoolSettingForKey:(NSString*)key defaultValue:(BOOL)defaultValue +{ + BOOL value = defaultValue; + id prefObj = [self cordovaSettingForKey:key]; + + if (prefObj != nil) { + value = [(NSNumber*)prefObj boolValue]; + } + + return value; +} + +- (CGFloat)cordovaFloatSettingForKey:(NSString*)key defaultValue:(CGFloat)defaultValue +{ + CGFloat value = defaultValue; + id prefObj = [self cordovaSettingForKey:key]; + + if (prefObj != nil) { + value = [prefObj floatValue]; + } + + return value; +} + +@end + +@implementation NSMutableDictionary (CordovaPreferences) + +- (void)setCordovaSetting:(id)value forKey:(NSString*)key +{ + [self setObject:value forKey:[key lowercaseString]]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/NSMutableArray+QueueAdditions.h b/msext.xcodeproj/CordovaLib/Classes/Public/NSMutableArray+QueueAdditions.h new file mode 100755 index 0000000..79e6516 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/NSMutableArray+QueueAdditions.h @@ -0,0 +1,29 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface NSMutableArray (QueueAdditions) + +- (id)cdv_pop; +- (id)cdv_queueHead; +- (id)cdv_dequeue; +- (void)cdv_enqueue:(id)obj; + +@end diff --git a/msext.xcodeproj/CordovaLib/Classes/Public/NSMutableArray+QueueAdditions.m b/msext.xcodeproj/CordovaLib/Classes/Public/NSMutableArray+QueueAdditions.m new file mode 100755 index 0000000..2b3acdc --- /dev/null +++ b/msext.xcodeproj/CordovaLib/Classes/Public/NSMutableArray+QueueAdditions.m @@ -0,0 +1,58 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "NSMutableArray+QueueAdditions.h" + +@implementation NSMutableArray (QueueAdditions) + +- (id)cdv_queueHead +{ + if ([self count] == 0) { + return nil; + } + + return [self objectAtIndex:0]; +} + +- (__autoreleasing id)cdv_dequeue +{ + if ([self count] == 0) { + return nil; + } + + id head = [self objectAtIndex:0]; + if (head != nil) { + // [[head retain] autorelease]; ARC - the __autoreleasing on the return value should so the same thing + [self removeObjectAtIndex:0]; + } + + return head; +} + +- (id)cdv_pop +{ + return [self cdv_dequeue]; +} + +- (void)cdv_enqueue:(id)object +{ + [self addObject:object]; +} + +@end diff --git a/msext.xcodeproj/CordovaLib/CordovaLib.xcodeproj/project.pbxproj b/msext.xcodeproj/CordovaLib/CordovaLib.xcodeproj/project.pbxproj new file mode 100755 index 0000000..b258171 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/CordovaLib.xcodeproj/project.pbxproj @@ -0,0 +1,526 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 30193A501AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 30193A4E1AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.m */; }; + 30193A511AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 30193A4F1AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.h */; }; + 3093E2231B16D6A3003F381A /* CDVIntentAndNavigationFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3093E2211B16D6A3003F381A /* CDVIntentAndNavigationFilter.h */; }; + 3093E2241B16D6A3003F381A /* CDVIntentAndNavigationFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = 3093E2221B16D6A3003F381A /* CDVIntentAndNavigationFilter.m */; }; + 7E7F69B61ABA35D8007546F4 /* CDVLocalStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95CFB1AB9028C008C4574 /* CDVLocalStorage.h */; }; + 7E7F69B81ABA368F007546F4 /* CDVUIWebViewEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D001AB9028C008C4574 /* CDVUIWebViewEngine.h */; }; + 7E7F69B91ABA3692007546F4 /* CDVHandleOpenURL.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95CF81AB9028C008C4574 /* CDVHandleOpenURL.h */; }; + 7ED95D021AB9028C008C4574 /* CDVDebug.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95CF21AB9028C008C4574 /* CDVDebug.h */; }; + 7ED95D031AB9028C008C4574 /* CDVJSON_private.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95CF31AB9028C008C4574 /* CDVJSON_private.h */; }; + 7ED95D041AB9028C008C4574 /* CDVJSON_private.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95CF41AB9028C008C4574 /* CDVJSON_private.m */; }; + 7ED95D051AB9028C008C4574 /* CDVPlugin+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95CF51AB9028C008C4574 /* CDVPlugin+Private.h */; }; + 7ED95D071AB9028C008C4574 /* CDVHandleOpenURL.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95CF91AB9028C008C4574 /* CDVHandleOpenURL.m */; }; + 7ED95D091AB9028C008C4574 /* CDVLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95CFC1AB9028C008C4574 /* CDVLocalStorage.m */; }; + 7ED95D0A1AB9028C008C4574 /* CDVUIWebViewDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95CFE1AB9028C008C4574 /* CDVUIWebViewDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D0B1AB9028C008C4574 /* CDVUIWebViewDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95CFF1AB9028C008C4574 /* CDVUIWebViewDelegate.m */; }; + 7ED95D0D1AB9028C008C4574 /* CDVUIWebViewEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D011AB9028C008C4574 /* CDVUIWebViewEngine.m */; }; + 7ED95D351AB9029B008C4574 /* CDV.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D0F1AB9029B008C4574 /* CDV.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D361AB9029B008C4574 /* CDVAppDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D101AB9029B008C4574 /* CDVAppDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D371AB9029B008C4574 /* CDVAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D111AB9029B008C4574 /* CDVAppDelegate.m */; }; + 7ED95D381AB9029B008C4574 /* CDVAvailability.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D121AB9029B008C4574 /* CDVAvailability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D391AB9029B008C4574 /* CDVAvailabilityDeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D131AB9029B008C4574 /* CDVAvailabilityDeprecated.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D3A1AB9029B008C4574 /* CDVCommandDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D141AB9029B008C4574 /* CDVCommandDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D3B1AB9029B008C4574 /* CDVCommandDelegateImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D151AB9029B008C4574 /* CDVCommandDelegateImpl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D3C1AB9029B008C4574 /* CDVCommandDelegateImpl.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D161AB9029B008C4574 /* CDVCommandDelegateImpl.m */; }; + 7ED95D3D1AB9029B008C4574 /* CDVCommandQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D171AB9029B008C4574 /* CDVCommandQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D3E1AB9029B008C4574 /* CDVCommandQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D181AB9029B008C4574 /* CDVCommandQueue.m */; }; + 7ED95D3F1AB9029B008C4574 /* CDVConfigParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D191AB9029B008C4574 /* CDVConfigParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D401AB9029B008C4574 /* CDVConfigParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D1A1AB9029B008C4574 /* CDVConfigParser.m */; }; + 7ED95D411AB9029B008C4574 /* CDVInvokedUrlCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D1B1AB9029B008C4574 /* CDVInvokedUrlCommand.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D421AB9029B008C4574 /* CDVInvokedUrlCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D1C1AB9029B008C4574 /* CDVInvokedUrlCommand.m */; }; + 7ED95D431AB9029B008C4574 /* CDVPlugin+Resources.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D1D1AB9029B008C4574 /* CDVPlugin+Resources.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D441AB9029B008C4574 /* CDVPlugin+Resources.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D1E1AB9029B008C4574 /* CDVPlugin+Resources.m */; }; + 7ED95D451AB9029B008C4574 /* CDVPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D1F1AB9029B008C4574 /* CDVPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D461AB9029B008C4574 /* CDVPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D201AB9029B008C4574 /* CDVPlugin.m */; }; + 7ED95D471AB9029B008C4574 /* CDVPluginResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D211AB9029B008C4574 /* CDVPluginResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D481AB9029B008C4574 /* CDVPluginResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D221AB9029B008C4574 /* CDVPluginResult.m */; }; + 7ED95D491AB9029B008C4574 /* CDVScreenOrientationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D231AB9029B008C4574 /* CDVScreenOrientationDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D4A1AB9029B008C4574 /* CDVTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D241AB9029B008C4574 /* CDVTimer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D4B1AB9029B008C4574 /* CDVTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D251AB9029B008C4574 /* CDVTimer.m */; }; + 7ED95D4C1AB9029B008C4574 /* CDVURLProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D261AB9029B008C4574 /* CDVURLProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D4D1AB9029B008C4574 /* CDVURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D271AB9029B008C4574 /* CDVURLProtocol.m */; }; + 7ED95D4E1AB9029B008C4574 /* CDVUserAgentUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D281AB9029B008C4574 /* CDVUserAgentUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D4F1AB9029B008C4574 /* CDVUserAgentUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D291AB9029B008C4574 /* CDVUserAgentUtil.m */; }; + 7ED95D501AB9029B008C4574 /* CDVViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D2A1AB9029B008C4574 /* CDVViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D511AB9029B008C4574 /* CDVViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D2B1AB9029B008C4574 /* CDVViewController.m */; }; + 7ED95D521AB9029B008C4574 /* CDVWebViewEngineProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D2C1AB9029B008C4574 /* CDVWebViewEngineProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D531AB9029B008C4574 /* CDVWhitelist.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D2D1AB9029B008C4574 /* CDVWhitelist.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D541AB9029B008C4574 /* CDVWhitelist.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D2E1AB9029B008C4574 /* CDVWhitelist.m */; }; + 7ED95D571AB9029B008C4574 /* NSDictionary+CordovaPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D311AB9029B008C4574 /* NSDictionary+CordovaPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D581AB9029B008C4574 /* NSDictionary+CordovaPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D321AB9029B008C4574 /* NSDictionary+CordovaPreferences.m */; }; + 7ED95D591AB9029B008C4574 /* NSMutableArray+QueueAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED95D331AB9029B008C4574 /* NSMutableArray+QueueAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ED95D5A1AB9029B008C4574 /* NSMutableArray+QueueAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ED95D341AB9029B008C4574 /* NSMutableArray+QueueAdditions.m */; }; + A3B082D41BB15CEA00D8DC35 /* CDVGestureHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = A3B082D21BB15CEA00D8DC35 /* CDVGestureHandler.h */; }; + A3B082D51BB15CEA00D8DC35 /* CDVGestureHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A3B082D31BB15CEA00D8DC35 /* CDVGestureHandler.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 30193A4E1AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVUIWebViewNavigationDelegate.m; sourceTree = ""; }; + 30193A4F1AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVUIWebViewNavigationDelegate.h; sourceTree = ""; }; + 30325A0B136B343700982B63 /* VERSION */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = VERSION; sourceTree = ""; }; + 3093E2211B16D6A3003F381A /* CDVIntentAndNavigationFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVIntentAndNavigationFilter.h; sourceTree = ""; }; + 3093E2221B16D6A3003F381A /* CDVIntentAndNavigationFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVIntentAndNavigationFilter.m; sourceTree = ""; }; + 68A32D7114102E1C006B237C /* libCordova.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCordova.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 7ED95CF21AB9028C008C4574 /* CDVDebug.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVDebug.h; sourceTree = ""; }; + 7ED95CF31AB9028C008C4574 /* CDVJSON_private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVJSON_private.h; sourceTree = ""; }; + 7ED95CF41AB9028C008C4574 /* CDVJSON_private.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVJSON_private.m; sourceTree = ""; }; + 7ED95CF51AB9028C008C4574 /* CDVPlugin+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CDVPlugin+Private.h"; sourceTree = ""; }; + 7ED95CF81AB9028C008C4574 /* CDVHandleOpenURL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVHandleOpenURL.h; sourceTree = ""; }; + 7ED95CF91AB9028C008C4574 /* CDVHandleOpenURL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVHandleOpenURL.m; sourceTree = ""; }; + 7ED95CFB1AB9028C008C4574 /* CDVLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVLocalStorage.h; sourceTree = ""; }; + 7ED95CFC1AB9028C008C4574 /* CDVLocalStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVLocalStorage.m; sourceTree = ""; }; + 7ED95CFE1AB9028C008C4574 /* CDVUIWebViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVUIWebViewDelegate.h; sourceTree = ""; }; + 7ED95CFF1AB9028C008C4574 /* CDVUIWebViewDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVUIWebViewDelegate.m; sourceTree = ""; }; + 7ED95D001AB9028C008C4574 /* CDVUIWebViewEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVUIWebViewEngine.h; sourceTree = ""; }; + 7ED95D011AB9028C008C4574 /* CDVUIWebViewEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVUIWebViewEngine.m; sourceTree = ""; }; + 7ED95D0F1AB9029B008C4574 /* CDV.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDV.h; sourceTree = ""; }; + 7ED95D101AB9029B008C4574 /* CDVAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVAppDelegate.h; sourceTree = ""; }; + 7ED95D111AB9029B008C4574 /* CDVAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVAppDelegate.m; sourceTree = ""; }; + 7ED95D121AB9029B008C4574 /* CDVAvailability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVAvailability.h; sourceTree = ""; }; + 7ED95D131AB9029B008C4574 /* CDVAvailabilityDeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVAvailabilityDeprecated.h; sourceTree = ""; }; + 7ED95D141AB9029B008C4574 /* CDVCommandDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVCommandDelegate.h; sourceTree = ""; }; + 7ED95D151AB9029B008C4574 /* CDVCommandDelegateImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVCommandDelegateImpl.h; sourceTree = ""; }; + 7ED95D161AB9029B008C4574 /* CDVCommandDelegateImpl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVCommandDelegateImpl.m; sourceTree = ""; }; + 7ED95D171AB9029B008C4574 /* CDVCommandQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVCommandQueue.h; sourceTree = ""; }; + 7ED95D181AB9029B008C4574 /* CDVCommandQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVCommandQueue.m; sourceTree = ""; }; + 7ED95D191AB9029B008C4574 /* CDVConfigParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVConfigParser.h; sourceTree = ""; }; + 7ED95D1A1AB9029B008C4574 /* CDVConfigParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVConfigParser.m; sourceTree = ""; }; + 7ED95D1B1AB9029B008C4574 /* CDVInvokedUrlCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVInvokedUrlCommand.h; sourceTree = ""; }; + 7ED95D1C1AB9029B008C4574 /* CDVInvokedUrlCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVInvokedUrlCommand.m; sourceTree = ""; }; + 7ED95D1D1AB9029B008C4574 /* CDVPlugin+Resources.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CDVPlugin+Resources.h"; sourceTree = ""; }; + 7ED95D1E1AB9029B008C4574 /* CDVPlugin+Resources.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CDVPlugin+Resources.m"; sourceTree = ""; }; + 7ED95D1F1AB9029B008C4574 /* CDVPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVPlugin.h; sourceTree = ""; }; + 7ED95D201AB9029B008C4574 /* CDVPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVPlugin.m; sourceTree = ""; }; + 7ED95D211AB9029B008C4574 /* CDVPluginResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVPluginResult.h; sourceTree = ""; }; + 7ED95D221AB9029B008C4574 /* CDVPluginResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVPluginResult.m; sourceTree = ""; }; + 7ED95D231AB9029B008C4574 /* CDVScreenOrientationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVScreenOrientationDelegate.h; sourceTree = ""; }; + 7ED95D241AB9029B008C4574 /* CDVTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVTimer.h; sourceTree = ""; }; + 7ED95D251AB9029B008C4574 /* CDVTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVTimer.m; sourceTree = ""; }; + 7ED95D261AB9029B008C4574 /* CDVURLProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVURLProtocol.h; sourceTree = ""; }; + 7ED95D271AB9029B008C4574 /* CDVURLProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVURLProtocol.m; sourceTree = ""; }; + 7ED95D281AB9029B008C4574 /* CDVUserAgentUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVUserAgentUtil.h; sourceTree = ""; }; + 7ED95D291AB9029B008C4574 /* CDVUserAgentUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVUserAgentUtil.m; sourceTree = ""; }; + 7ED95D2A1AB9029B008C4574 /* CDVViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVViewController.h; sourceTree = ""; }; + 7ED95D2B1AB9029B008C4574 /* CDVViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVViewController.m; sourceTree = ""; }; + 7ED95D2C1AB9029B008C4574 /* CDVWebViewEngineProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVWebViewEngineProtocol.h; sourceTree = ""; }; + 7ED95D2D1AB9029B008C4574 /* CDVWhitelist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVWhitelist.h; sourceTree = ""; }; + 7ED95D2E1AB9029B008C4574 /* CDVWhitelist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVWhitelist.m; sourceTree = ""; }; + 7ED95D311AB9029B008C4574 /* NSDictionary+CordovaPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+CordovaPreferences.h"; sourceTree = ""; }; + 7ED95D321AB9029B008C4574 /* NSDictionary+CordovaPreferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+CordovaPreferences.m"; sourceTree = ""; }; + 7ED95D331AB9029B008C4574 /* NSMutableArray+QueueAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+QueueAdditions.h"; sourceTree = ""; }; + 7ED95D341AB9029B008C4574 /* NSMutableArray+QueueAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+QueueAdditions.m"; sourceTree = ""; }; + A3B082D21BB15CEA00D8DC35 /* CDVGestureHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDVGestureHandler.h; sourceTree = ""; }; + A3B082D31BB15CEA00D8DC35 /* CDVGestureHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVGestureHandler.m; sourceTree = ""; }; + AA747D9E0F9514B9006C5449 /* CordovaLib_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CordovaLib_Prefix.pch; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D2AAC07C0554694100DB518D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 034768DFFF38A50411DB9C8B /* Products */ = { + isa = PBXGroup; + children = ( + 68A32D7114102E1C006B237C /* libCordova.a */, + ); + name = Products; + sourceTree = CORDOVALIB; + }; + 0867D691FE84028FC02AAC07 /* CordovaLib */ = { + isa = PBXGroup; + children = ( + 7ED95D0E1AB9029B008C4574 /* Public */, + 7ED95CF11AB9028C008C4574 /* Private */, + 034768DFFF38A50411DB9C8B /* Products */, + 30325A0B136B343700982B63 /* VERSION */, + ); + name = CordovaLib; + sourceTree = ""; + }; + 3093E2201B16D6A3003F381A /* CDVIntentAndNavigationFilter */ = { + isa = PBXGroup; + children = ( + 3093E2211B16D6A3003F381A /* CDVIntentAndNavigationFilter.h */, + 3093E2221B16D6A3003F381A /* CDVIntentAndNavigationFilter.m */, + ); + path = CDVIntentAndNavigationFilter; + sourceTree = ""; + }; + 7ED95CF11AB9028C008C4574 /* Private */ = { + isa = PBXGroup; + children = ( + AA747D9E0F9514B9006C5449 /* CordovaLib_Prefix.pch */, + 7ED95CF21AB9028C008C4574 /* CDVDebug.h */, + 7ED95CF31AB9028C008C4574 /* CDVJSON_private.h */, + 7ED95CF41AB9028C008C4574 /* CDVJSON_private.m */, + 7ED95CF51AB9028C008C4574 /* CDVPlugin+Private.h */, + 7ED95CF61AB9028C008C4574 /* Plugins */, + ); + name = Private; + path = Classes/Private; + sourceTree = ""; + }; + 7ED95CF61AB9028C008C4574 /* Plugins */ = { + isa = PBXGroup; + children = ( + A3B082D11BB15CEA00D8DC35 /* CDVGestureHandler */, + 3093E2201B16D6A3003F381A /* CDVIntentAndNavigationFilter */, + 7ED95CF71AB9028C008C4574 /* CDVHandleOpenURL */, + 7ED95CFA1AB9028C008C4574 /* CDVLocalStorage */, + 7ED95CFD1AB9028C008C4574 /* CDVUIWebViewEngine */, + ); + path = Plugins; + sourceTree = ""; + }; + 7ED95CF71AB9028C008C4574 /* CDVHandleOpenURL */ = { + isa = PBXGroup; + children = ( + 7ED95CF81AB9028C008C4574 /* CDVHandleOpenURL.h */, + 7ED95CF91AB9028C008C4574 /* CDVHandleOpenURL.m */, + ); + path = CDVHandleOpenURL; + sourceTree = ""; + }; + 7ED95CFA1AB9028C008C4574 /* CDVLocalStorage */ = { + isa = PBXGroup; + children = ( + 7ED95CFB1AB9028C008C4574 /* CDVLocalStorage.h */, + 7ED95CFC1AB9028C008C4574 /* CDVLocalStorage.m */, + ); + path = CDVLocalStorage; + sourceTree = ""; + }; + 7ED95CFD1AB9028C008C4574 /* CDVUIWebViewEngine */ = { + isa = PBXGroup; + children = ( + 30193A4E1AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.m */, + 30193A4F1AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.h */, + 7ED95CFE1AB9028C008C4574 /* CDVUIWebViewDelegate.h */, + 7ED95CFF1AB9028C008C4574 /* CDVUIWebViewDelegate.m */, + 7ED95D001AB9028C008C4574 /* CDVUIWebViewEngine.h */, + 7ED95D011AB9028C008C4574 /* CDVUIWebViewEngine.m */, + ); + path = CDVUIWebViewEngine; + sourceTree = ""; + }; + 7ED95D0E1AB9029B008C4574 /* Public */ = { + isa = PBXGroup; + children = ( + 7ED95D0F1AB9029B008C4574 /* CDV.h */, + 7ED95D101AB9029B008C4574 /* CDVAppDelegate.h */, + 7ED95D111AB9029B008C4574 /* CDVAppDelegate.m */, + 7ED95D121AB9029B008C4574 /* CDVAvailability.h */, + 7ED95D131AB9029B008C4574 /* CDVAvailabilityDeprecated.h */, + 7ED95D141AB9029B008C4574 /* CDVCommandDelegate.h */, + 7ED95D151AB9029B008C4574 /* CDVCommandDelegateImpl.h */, + 7ED95D161AB9029B008C4574 /* CDVCommandDelegateImpl.m */, + 7ED95D171AB9029B008C4574 /* CDVCommandQueue.h */, + 7ED95D181AB9029B008C4574 /* CDVCommandQueue.m */, + 7ED95D191AB9029B008C4574 /* CDVConfigParser.h */, + 7ED95D1A1AB9029B008C4574 /* CDVConfigParser.m */, + 7ED95D1B1AB9029B008C4574 /* CDVInvokedUrlCommand.h */, + 7ED95D1C1AB9029B008C4574 /* CDVInvokedUrlCommand.m */, + 7ED95D1D1AB9029B008C4574 /* CDVPlugin+Resources.h */, + 7ED95D1E1AB9029B008C4574 /* CDVPlugin+Resources.m */, + 7ED95D1F1AB9029B008C4574 /* CDVPlugin.h */, + 7ED95D201AB9029B008C4574 /* CDVPlugin.m */, + 7ED95D211AB9029B008C4574 /* CDVPluginResult.h */, + 7ED95D221AB9029B008C4574 /* CDVPluginResult.m */, + 7ED95D231AB9029B008C4574 /* CDVScreenOrientationDelegate.h */, + 7ED95D241AB9029B008C4574 /* CDVTimer.h */, + 7ED95D251AB9029B008C4574 /* CDVTimer.m */, + 7ED95D261AB9029B008C4574 /* CDVURLProtocol.h */, + 7ED95D271AB9029B008C4574 /* CDVURLProtocol.m */, + 7ED95D281AB9029B008C4574 /* CDVUserAgentUtil.h */, + 7ED95D291AB9029B008C4574 /* CDVUserAgentUtil.m */, + 7ED95D2A1AB9029B008C4574 /* CDVViewController.h */, + 7ED95D2B1AB9029B008C4574 /* CDVViewController.m */, + 7ED95D2C1AB9029B008C4574 /* CDVWebViewEngineProtocol.h */, + 7ED95D2D1AB9029B008C4574 /* CDVWhitelist.h */, + 7ED95D2E1AB9029B008C4574 /* CDVWhitelist.m */, + 7ED95D311AB9029B008C4574 /* NSDictionary+CordovaPreferences.h */, + 7ED95D321AB9029B008C4574 /* NSDictionary+CordovaPreferences.m */, + 7ED95D331AB9029B008C4574 /* NSMutableArray+QueueAdditions.h */, + 7ED95D341AB9029B008C4574 /* NSMutableArray+QueueAdditions.m */, + ); + name = Public; + path = Classes/Public; + sourceTree = ""; + }; + A3B082D11BB15CEA00D8DC35 /* CDVGestureHandler */ = { + isa = PBXGroup; + children = ( + A3B082D21BB15CEA00D8DC35 /* CDVGestureHandler.h */, + A3B082D31BB15CEA00D8DC35 /* CDVGestureHandler.m */, + ); + path = CDVGestureHandler; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D2AAC07A0554694100DB518D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ED95D521AB9029B008C4574 /* CDVWebViewEngineProtocol.h in Headers */, + 7ED95D491AB9029B008C4574 /* CDVScreenOrientationDelegate.h in Headers */, + 7ED95D351AB9029B008C4574 /* CDV.h in Headers */, + A3B082D41BB15CEA00D8DC35 /* CDVGestureHandler.h in Headers */, + 7ED95D3B1AB9029B008C4574 /* CDVCommandDelegateImpl.h in Headers */, + 7ED95D3D1AB9029B008C4574 /* CDVCommandQueue.h in Headers */, + 7ED95D531AB9029B008C4574 /* CDVWhitelist.h in Headers */, + 7ED95D361AB9029B008C4574 /* CDVAppDelegate.h in Headers */, + 7ED95D431AB9029B008C4574 /* CDVPlugin+Resources.h in Headers */, + 7ED95D381AB9029B008C4574 /* CDVAvailability.h in Headers */, + 7ED95D0A1AB9028C008C4574 /* CDVUIWebViewDelegate.h in Headers */, + 7ED95D471AB9029B008C4574 /* CDVPluginResult.h in Headers */, + 7ED95D591AB9029B008C4574 /* NSMutableArray+QueueAdditions.h in Headers */, + 7ED95D411AB9029B008C4574 /* CDVInvokedUrlCommand.h in Headers */, + 7ED95D571AB9029B008C4574 /* NSDictionary+CordovaPreferences.h in Headers */, + 7ED95D451AB9029B008C4574 /* CDVPlugin.h in Headers */, + 7ED95D4C1AB9029B008C4574 /* CDVURLProtocol.h in Headers */, + 7ED95D3A1AB9029B008C4574 /* CDVCommandDelegate.h in Headers */, + 7ED95D391AB9029B008C4574 /* CDVAvailabilityDeprecated.h in Headers */, + 7ED95D4E1AB9029B008C4574 /* CDVUserAgentUtil.h in Headers */, + 7ED95D4A1AB9029B008C4574 /* CDVTimer.h in Headers */, + 7ED95D3F1AB9029B008C4574 /* CDVConfigParser.h in Headers */, + 7ED95D501AB9029B008C4574 /* CDVViewController.h in Headers */, + 7ED95D031AB9028C008C4574 /* CDVJSON_private.h in Headers */, + 7ED95D021AB9028C008C4574 /* CDVDebug.h in Headers */, + 7ED95D051AB9028C008C4574 /* CDVPlugin+Private.h in Headers */, + 7E7F69B61ABA35D8007546F4 /* CDVLocalStorage.h in Headers */, + 3093E2231B16D6A3003F381A /* CDVIntentAndNavigationFilter.h in Headers */, + 7E7F69B81ABA368F007546F4 /* CDVUIWebViewEngine.h in Headers */, + 7E7F69B91ABA3692007546F4 /* CDVHandleOpenURL.h in Headers */, + 30193A511AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D2AAC07D0554694100DB518D /* CordovaLib */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB921E08733DC00010E9CD /* Build configuration list for PBXNativeTarget "CordovaLib" */; + buildPhases = ( + D2AAC07A0554694100DB518D /* Headers */, + D2AAC07B0554694100DB518D /* Sources */, + D2AAC07C0554694100DB518D /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CordovaLib; + productName = CordovaLib; + productReference = 68A32D7114102E1C006B237C /* libCordova.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0867D690FE84028FC02AAC07 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + }; + buildConfigurationList = 1DEB922208733DC00010E9CD /* Build configuration list for PBXProject "CordovaLib" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + en, + ); + mainGroup = 0867D691FE84028FC02AAC07 /* CordovaLib */; + productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2AAC07D0554694100DB518D /* CordovaLib */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + D2AAC07B0554694100DB518D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ED95D511AB9029B008C4574 /* CDVViewController.m in Sources */, + 7ED95D581AB9029B008C4574 /* NSDictionary+CordovaPreferences.m in Sources */, + 7ED95D371AB9029B008C4574 /* CDVAppDelegate.m in Sources */, + 7ED95D0B1AB9028C008C4574 /* CDVUIWebViewDelegate.m in Sources */, + 7ED95D3C1AB9029B008C4574 /* CDVCommandDelegateImpl.m in Sources */, + 7ED95D041AB9028C008C4574 /* CDVJSON_private.m in Sources */, + 7ED95D541AB9029B008C4574 /* CDVWhitelist.m in Sources */, + 7ED95D421AB9029B008C4574 /* CDVInvokedUrlCommand.m in Sources */, + 7ED95D4B1AB9029B008C4574 /* CDVTimer.m in Sources */, + 7ED95D4F1AB9029B008C4574 /* CDVUserAgentUtil.m in Sources */, + 7ED95D401AB9029B008C4574 /* CDVConfigParser.m in Sources */, + A3B082D51BB15CEA00D8DC35 /* CDVGestureHandler.m in Sources */, + 7ED95D071AB9028C008C4574 /* CDVHandleOpenURL.m in Sources */, + 30193A501AE6350A0069A75F /* CDVUIWebViewNavigationDelegate.m in Sources */, + 7ED95D5A1AB9029B008C4574 /* NSMutableArray+QueueAdditions.m in Sources */, + 7ED95D3E1AB9029B008C4574 /* CDVCommandQueue.m in Sources */, + 7ED95D481AB9029B008C4574 /* CDVPluginResult.m in Sources */, + 7ED95D441AB9029B008C4574 /* CDVPlugin+Resources.m in Sources */, + 7ED95D4D1AB9029B008C4574 /* CDVURLProtocol.m in Sources */, + 7ED95D0D1AB9028C008C4574 /* CDVUIWebViewEngine.m in Sources */, + 7ED95D461AB9029B008C4574 /* CDVPlugin.m in Sources */, + 7ED95D091AB9028C008C4574 /* CDVLocalStorage.m in Sources */, + 3093E2241B16D6A3003F381A /* CDVIntentAndNavigationFilter.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1DEB921F08733DC00010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DSTROOT = "/tmp/$(PROJECT_NAME).dst"; + GCC_DYNAMIC_NO_PIC = NO; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = CordovaLib_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_THUMB_SUPPORT = NO; + GCC_VERSION = ""; + INSTALL_PATH = /usr/local/lib; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = Cordova; + PUBLIC_HEADERS_FOLDER_PATH = include/Cordova; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 1DEB922008733DC00010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + DSTROOT = "/tmp/$(PROJECT_NAME).dst"; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = CordovaLib_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_THUMB_SUPPORT = NO; + GCC_VERSION = ""; + INSTALL_PATH = /usr/local/lib; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + PRODUCT_NAME = Cordova; + PUBLIC_HEADERS_FOLDER_PATH = include/Cordova; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 1DEB922308733DC00010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_THUMB_SUPPORT = NO; + GCC_VERSION = ""; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DDEBUG"; + PUBLIC_HEADERS_FOLDER_PATH = include/Cordova; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = Debug; + }; + 1DEB922408733DC00010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_THUMB_SUPPORT = NO; + GCC_VERSION = ""; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = NO; + PUBLIC_HEADERS_FOLDER_PATH = include/Cordova; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB921E08733DC00010E9CD /* Build configuration list for PBXNativeTarget "CordovaLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB921F08733DC00010E9CD /* Debug */, + 1DEB922008733DC00010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB922208733DC00010E9CD /* Build configuration list for PBXProject "CordovaLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB922308733DC00010E9CD /* Debug */, + 1DEB922408733DC00010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0867D690FE84028FC02AAC07 /* Project object */; +} diff --git a/msext.xcodeproj/CordovaLib/CordovaLib.xcodeproj/xcuserdata/chao.xcuserdatad/xcschemes/xcschememanagement.plist b/msext.xcodeproj/CordovaLib/CordovaLib.xcodeproj/xcuserdata/chao.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000..07842c7 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/CordovaLib.xcodeproj/xcuserdata/chao.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SuppressBuildableAutocreation + + D2AAC07D0554694100DB518D + + primary + + + + + diff --git a/msext.xcodeproj/CordovaLib/CordovaLib_Prefix.pch b/msext.xcodeproj/CordovaLib/CordovaLib_Prefix.pch new file mode 100755 index 0000000..9545580 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/CordovaLib_Prefix.pch @@ -0,0 +1,22 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#ifdef __OBJC__ + #import +#endif diff --git a/msext.xcodeproj/CordovaLib/VERSION b/msext.xcodeproj/CordovaLib/VERSION new file mode 100755 index 0000000..2582ddd --- /dev/null +++ b/msext.xcodeproj/CordovaLib/VERSION @@ -0,0 +1 @@ +4.1.1 \ No newline at end of file diff --git a/msext.xcodeproj/CordovaLib/cordova.js b/msext.xcodeproj/CordovaLib/cordova.js new file mode 100755 index 0000000..0113151 --- /dev/null +++ b/msext.xcodeproj/CordovaLib/cordova.js @@ -0,0 +1,1911 @@ +// Platform: ios +// 2fd4bcb84048415922d13d80d35b8d1668e8e150 +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +;(function() { +var PLATFORM_VERSION_BUILD_LABEL = '4.1.1'; +// file: src/scripts/require.js + +/*jshint -W079 */ +/*jshint -W020 */ + +var require, + define; + +(function () { + var modules = {}, + // Stack of moduleIds currently being built. + requireStack = [], + // Map of module ID -> index into requireStack of modules currently being built. + inProgressModules = {}, + SEPARATOR = "."; + + + + function build(module) { + var factory = module.factory, + localRequire = function (id) { + var resultantId = id; + //Its a relative path, so lop off the last portion and add the id (minus "./") + if (id.charAt(0) === ".") { + resultantId = module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + SEPARATOR + id.slice(2); + } + return require(resultantId); + }; + module.exports = {}; + delete module.factory; + factory(localRequire, module.exports, module); + return module.exports; + } + + require = function (id) { + if (!modules[id]) { + throw "module " + id + " not found"; + } else if (id in inProgressModules) { + var cycle = requireStack.slice(inProgressModules[id]).join('->') + '->' + id; + throw "Cycle in require graph: " + cycle; + } + if (modules[id].factory) { + try { + inProgressModules[id] = requireStack.length; + requireStack.push(id); + return build(modules[id]); + } finally { + delete inProgressModules[id]; + requireStack.pop(); + } + } + return modules[id].exports; + }; + + define = function (id, factory) { + if (modules[id]) { + throw "module " + id + " already defined"; + } + + modules[id] = { + id: id, + factory: factory + }; + }; + + define.remove = function (id) { + delete modules[id]; + }; + + define.moduleMap = modules; +})(); + +//Export for use in node +if (typeof module === "object" && typeof require === "function") { + module.exports.require = require; + module.exports.define = define; +} + +// file: src/cordova.js +define("cordova", function(require, exports, module) { + +// Workaround for Windows 10 in hosted environment case +// http://www.w3.org/html/wg/drafts/html/master/browsers.html#named-access-on-the-window-object +if (window.cordova && !(window.cordova instanceof HTMLElement)) { + throw new Error("cordova already defined"); +} + + +var channel = require('cordova/channel'); +var platform = require('cordova/platform'); + + +/** + * Intercept calls to addEventListener + removeEventListener and handle deviceready, + * resume, and pause events. + */ +var m_document_addEventListener = document.addEventListener; +var m_document_removeEventListener = document.removeEventListener; +var m_window_addEventListener = window.addEventListener; +var m_window_removeEventListener = window.removeEventListener; + +/** + * Houses custom event handlers to intercept on document + window event listeners. + */ +var documentEventHandlers = {}, + windowEventHandlers = {}; + +document.addEventListener = function(evt, handler, capture) { + var e = evt.toLowerCase(); + if (typeof documentEventHandlers[e] != 'undefined') { + documentEventHandlers[e].subscribe(handler); + } else { + m_document_addEventListener.call(document, evt, handler, capture); + } +}; + +window.addEventListener = function(evt, handler, capture) { + var e = evt.toLowerCase(); + if (typeof windowEventHandlers[e] != 'undefined') { + windowEventHandlers[e].subscribe(handler); + } else { + m_window_addEventListener.call(window, evt, handler, capture); + } +}; + +document.removeEventListener = function(evt, handler, capture) { + var e = evt.toLowerCase(); + // If unsubscribing from an event that is handled by a plugin + if (typeof documentEventHandlers[e] != "undefined") { + documentEventHandlers[e].unsubscribe(handler); + } else { + m_document_removeEventListener.call(document, evt, handler, capture); + } +}; + +window.removeEventListener = function(evt, handler, capture) { + var e = evt.toLowerCase(); + // If unsubscribing from an event that is handled by a plugin + if (typeof windowEventHandlers[e] != "undefined") { + windowEventHandlers[e].unsubscribe(handler); + } else { + m_window_removeEventListener.call(window, evt, handler, capture); + } +}; + +function createEvent(type, data) { + var event = document.createEvent('Events'); + event.initEvent(type, false, false); + if (data) { + for (var i in data) { + if (data.hasOwnProperty(i)) { + event[i] = data[i]; + } + } + } + return event; +} + + +var cordova = { + define:define, + require:require, + version:PLATFORM_VERSION_BUILD_LABEL, + platformVersion:PLATFORM_VERSION_BUILD_LABEL, + platformId:platform.id, + /** + * Methods to add/remove your own addEventListener hijacking on document + window. + */ + addWindowEventHandler:function(event) { + return (windowEventHandlers[event] = channel.create(event)); + }, + addStickyDocumentEventHandler:function(event) { + return (documentEventHandlers[event] = channel.createSticky(event)); + }, + addDocumentEventHandler:function(event) { + return (documentEventHandlers[event] = channel.create(event)); + }, + removeWindowEventHandler:function(event) { + delete windowEventHandlers[event]; + }, + removeDocumentEventHandler:function(event) { + delete documentEventHandlers[event]; + }, + /** + * Retrieve original event handlers that were replaced by Cordova + * + * @return object + */ + getOriginalHandlers: function() { + return {'document': {'addEventListener': m_document_addEventListener, 'removeEventListener': m_document_removeEventListener}, + 'window': {'addEventListener': m_window_addEventListener, 'removeEventListener': m_window_removeEventListener}}; + }, + /** + * Method to fire event from native code + * bNoDetach is required for events which cause an exception which needs to be caught in native code + */ + fireDocumentEvent: function(type, data, bNoDetach) { + var evt = createEvent(type, data); + if (typeof documentEventHandlers[type] != 'undefined') { + if( bNoDetach ) { + documentEventHandlers[type].fire(evt); + } + else { + setTimeout(function() { + // Fire deviceready on listeners that were registered before cordova.js was loaded. + if (type == 'deviceready') { + document.dispatchEvent(evt); + } + documentEventHandlers[type].fire(evt); + }, 0); + } + } else { + document.dispatchEvent(evt); + } + }, + fireWindowEvent: function(type, data) { + var evt = createEvent(type,data); + if (typeof windowEventHandlers[type] != 'undefined') { + setTimeout(function() { + windowEventHandlers[type].fire(evt); + }, 0); + } else { + window.dispatchEvent(evt); + } + }, + + /** + * Plugin callback mechanism. + */ + // Randomize the starting callbackId to avoid collisions after refreshing or navigating. + // This way, it's very unlikely that any new callback would get the same callbackId as an old callback. + callbackId: Math.floor(Math.random() * 2000000000), + callbacks: {}, + callbackStatus: { + NO_RESULT: 0, + OK: 1, + CLASS_NOT_FOUND_EXCEPTION: 2, + ILLEGAL_ACCESS_EXCEPTION: 3, + INSTANTIATION_EXCEPTION: 4, + MALFORMED_URL_EXCEPTION: 5, + IO_EXCEPTION: 6, + INVALID_ACTION: 7, + JSON_EXCEPTION: 8, + ERROR: 9 + }, + + /** + * Called by native code when returning successful result from an action. + */ + callbackSuccess: function(callbackId, args) { + cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback); + }, + + /** + * Called by native code when returning error result from an action. + */ + callbackError: function(callbackId, args) { + // TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative. + // Derive success from status. + cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback); + }, + + /** + * Called by native code when returning the result from an action. + */ + callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) { + try { + var callback = cordova.callbacks[callbackId]; + if (callback) { + if (isSuccess && status == cordova.callbackStatus.OK) { + callback.success && callback.success.apply(null, args); + } else if (!isSuccess) { + callback.fail && callback.fail.apply(null, args); + } + /* + else + Note, this case is intentionally not caught. + this can happen if isSuccess is true, but callbackStatus is NO_RESULT + which is used to remove a callback from the list without calling the callbacks + typically keepCallback is false in this case + */ + // Clear callback if not expecting any more results + if (!keepCallback) { + delete cordova.callbacks[callbackId]; + } + } + } + catch (err) { + var msg = "Error in " + (isSuccess ? "Success" : "Error") + " callbackId: " + callbackId + " : " + err; + console && console.log && console.log(msg); + cordova.fireWindowEvent("cordovacallbackerror", { 'message': msg }); + throw err; + } + }, + addConstructor: function(func) { + channel.onCordovaReady.subscribe(function() { + try { + func(); + } catch(e) { + console.log("Failed to run constructor: " + e); + } + }); + } +}; + + +module.exports = cordova; + +}); + +// file: src/common/argscheck.js +define("cordova/argscheck", function(require, exports, module) { + +var utils = require('cordova/utils'); + +var moduleExports = module.exports; + +var typeMap = { + 'A': 'Array', + 'D': 'Date', + 'N': 'Number', + 'S': 'String', + 'F': 'Function', + 'O': 'Object' +}; + +function extractParamName(callee, argIndex) { + return (/.*?\((.*?)\)/).exec(callee)[1].split(', ')[argIndex]; +} + +function checkArgs(spec, functionName, args, opt_callee) { + if (!moduleExports.enableChecks) { + return; + } + var errMsg = null; + var typeName; + for (var i = 0; i < spec.length; ++i) { + var c = spec.charAt(i), + cUpper = c.toUpperCase(), + arg = args[i]; + // Asterix means allow anything. + if (c == '*') { + continue; + } + typeName = utils.typeName(arg); + if ((arg === null || arg === undefined) && c == cUpper) { + continue; + } + if (typeName != typeMap[cUpper]) { + errMsg = 'Expected ' + typeMap[cUpper]; + break; + } + } + if (errMsg) { + errMsg += ', but got ' + typeName + '.'; + errMsg = 'Wrong type for parameter "' + extractParamName(opt_callee || args.callee, i) + '" of ' + functionName + ': ' + errMsg; + // Don't log when running unit tests. + if (typeof jasmine == 'undefined') { + console.error(errMsg); + } + throw TypeError(errMsg); + } +} + +function getValue(value, defaultValue) { + return value === undefined ? defaultValue : value; +} + +moduleExports.checkArgs = checkArgs; +moduleExports.getValue = getValue; +moduleExports.enableChecks = true; + + +}); + +// file: src/common/base64.js +define("cordova/base64", function(require, exports, module) { + +var base64 = exports; + +base64.fromArrayBuffer = function(arrayBuffer) { + var array = new Uint8Array(arrayBuffer); + return uint8ToBase64(array); +}; + +base64.toArrayBuffer = function(str) { + var decodedStr = typeof atob != 'undefined' ? atob(str) : new Buffer(str,'base64').toString('binary'); + var arrayBuffer = new ArrayBuffer(decodedStr.length); + var array = new Uint8Array(arrayBuffer); + for (var i=0, len=decodedStr.length; i < len; i++) { + array[i] = decodedStr.charCodeAt(i); + } + return arrayBuffer; +}; + +//------------------------------------------------------------------------------ + +/* This code is based on the performance tests at http://jsperf.com/b64tests + * This 12-bit-at-a-time algorithm was the best performing version on all + * platforms tested. + */ + +var b64_6bit = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var b64_12bit; + +var b64_12bitTable = function() { + b64_12bit = []; + for (var i=0; i<64; i++) { + for (var j=0; j<64; j++) { + b64_12bit[i*64+j] = b64_6bit[i] + b64_6bit[j]; + } + } + b64_12bitTable = function() { return b64_12bit; }; + return b64_12bit; +}; + +function uint8ToBase64(rawData) { + var numBytes = rawData.byteLength; + var output=""; + var segment; + var table = b64_12bitTable(); + for (var i=0;i> 12]; + output += table[segment & 0xfff]; + } + if (numBytes - i == 2) { + segment = (rawData[i] << 16) + (rawData[i+1] << 8); + output += table[segment >> 12]; + output += b64_6bit[(segment & 0xfff) >> 6]; + output += '='; + } else if (numBytes - i == 1) { + segment = (rawData[i] << 16); + output += table[segment >> 12]; + output += '=='; + } + return output; +} + +}); + +// file: src/common/builder.js +define("cordova/builder", function(require, exports, module) { + +var utils = require('cordova/utils'); + +function each(objects, func, context) { + for (var prop in objects) { + if (objects.hasOwnProperty(prop)) { + func.apply(context, [objects[prop], prop]); + } + } +} + +function clobber(obj, key, value) { + exports.replaceHookForTesting(obj, key); + var needsProperty = false; + try { + obj[key] = value; + } catch (e) { + needsProperty = true; + } + // Getters can only be overridden by getters. + if (needsProperty || obj[key] !== value) { + utils.defineGetter(obj, key, function() { + return value; + }); + } +} + +function assignOrWrapInDeprecateGetter(obj, key, value, message) { + if (message) { + utils.defineGetter(obj, key, function() { + console.log(message); + delete obj[key]; + clobber(obj, key, value); + return value; + }); + } else { + clobber(obj, key, value); + } +} + +function include(parent, objects, clobber, merge) { + each(objects, function (obj, key) { + try { + var result = obj.path ? require(obj.path) : {}; + + if (clobber) { + // Clobber if it doesn't exist. + if (typeof parent[key] === 'undefined') { + assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated); + } else if (typeof obj.path !== 'undefined') { + // If merging, merge properties onto parent, otherwise, clobber. + if (merge) { + recursiveMerge(parent[key], result); + } else { + assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated); + } + } + result = parent[key]; + } else { + // Overwrite if not currently defined. + if (typeof parent[key] == 'undefined') { + assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated); + } else { + // Set result to what already exists, so we can build children into it if they exist. + result = parent[key]; + } + } + + if (obj.children) { + include(result, obj.children, clobber, merge); + } + } catch(e) { + utils.alert('Exception building Cordova JS globals: ' + e + ' for key "' + key + '"'); + } + }); +} + +/** + * Merge properties from one object onto another recursively. Properties from + * the src object will overwrite existing target property. + * + * @param target Object to merge properties into. + * @param src Object to merge properties from. + */ +function recursiveMerge(target, src) { + for (var prop in src) { + if (src.hasOwnProperty(prop)) { + if (target.prototype && target.prototype.constructor === target) { + // If the target object is a constructor override off prototype. + clobber(target.prototype, prop, src[prop]); + } else { + if (typeof src[prop] === 'object' && typeof target[prop] === 'object') { + recursiveMerge(target[prop], src[prop]); + } else { + clobber(target, prop, src[prop]); + } + } + } + } +} + +exports.buildIntoButDoNotClobber = function(objects, target) { + include(target, objects, false, false); +}; +exports.buildIntoAndClobber = function(objects, target) { + include(target, objects, true, false); +}; +exports.buildIntoAndMerge = function(objects, target) { + include(target, objects, true, true); +}; +exports.recursiveMerge = recursiveMerge; +exports.assignOrWrapInDeprecateGetter = assignOrWrapInDeprecateGetter; +exports.replaceHookForTesting = function() {}; + +}); + +// file: src/common/channel.js +define("cordova/channel", function(require, exports, module) { + +var utils = require('cordova/utils'), + nextGuid = 1; + +/** + * Custom pub-sub "channel" that can have functions subscribed to it + * This object is used to define and control firing of events for + * cordova initialization, as well as for custom events thereafter. + * + * The order of events during page load and Cordova startup is as follows: + * + * onDOMContentLoaded* Internal event that is received when the web page is loaded and parsed. + * onNativeReady* Internal event that indicates the Cordova native side is ready. + * onCordovaReady* Internal event fired when all Cordova JavaScript objects have been created. + * onDeviceReady* User event fired to indicate that Cordova is ready + * onResume User event fired to indicate a start/resume lifecycle event + * onPause User event fired to indicate a pause lifecycle event + * + * The events marked with an * are sticky. Once they have fired, they will stay in the fired state. + * All listeners that subscribe after the event is fired will be executed right away. + * + * The only Cordova events that user code should register for are: + * deviceready Cordova native code is initialized and Cordova APIs can be called from JavaScript + * pause App has moved to background + * resume App has returned to foreground + * + * Listeners can be registered as: + * document.addEventListener("deviceready", myDeviceReadyListener, false); + * document.addEventListener("resume", myResumeListener, false); + * document.addEventListener("pause", myPauseListener, false); + * + * The DOM lifecycle events should be used for saving and restoring state + * window.onload + * window.onunload + * + */ + +/** + * Channel + * @constructor + * @param type String the channel name + */ +var Channel = function(type, sticky) { + this.type = type; + // Map of guid -> function. + this.handlers = {}; + // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired. + this.state = sticky ? 1 : 0; + // Used in sticky mode to remember args passed to fire(). + this.fireArgs = null; + // Used by onHasSubscribersChange to know if there are any listeners. + this.numHandlers = 0; + // Function that is called when the first listener is subscribed, or when + // the last listener is unsubscribed. + this.onHasSubscribersChange = null; +}, + channel = { + /** + * Calls the provided function only after all of the channels specified + * have been fired. All channels must be sticky channels. + */ + join: function(h, c) { + var len = c.length, + i = len, + f = function() { + if (!(--i)) h(); + }; + for (var j=0; jNative messages. + isInContextOfEvalJs = 0, + failSafeTimerId = 0; + +function massageArgsJsToNative(args) { + if (!args || utils.typeName(args) != 'Array') { + return args; + } + var ret = []; + args.forEach(function(arg, i) { + if (utils.typeName(arg) == 'ArrayBuffer') { + ret.push({ + 'CDVType': 'ArrayBuffer', + 'data': base64.fromArrayBuffer(arg) + }); + } else { + ret.push(arg); + } + }); + return ret; +} + +function massageMessageNativeToJs(message) { + if (message.CDVType == 'ArrayBuffer') { + var stringToArrayBuffer = function(str) { + var ret = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + ret[i] = str.charCodeAt(i); + } + return ret.buffer; + }; + var base64ToArrayBuffer = function(b64) { + return stringToArrayBuffer(atob(b64)); + }; + message = base64ToArrayBuffer(message.data); + } + return message; +} + +function convertMessageToArgsNativeToJs(message) { + var args = []; + if (!message || !message.hasOwnProperty('CDVType')) { + args.push(message); + } else if (message.CDVType == 'MultiPart') { + message.messages.forEach(function(e) { + args.push(massageMessageNativeToJs(e)); + }); + } else { + args.push(massageMessageNativeToJs(message)); + } + return args; +} + +function iOSExec() { + + var successCallback, failCallback, service, action, actionArgs; + var callbackId = null; + if (typeof arguments[0] !== 'string') { + // FORMAT ONE + successCallback = arguments[0]; + failCallback = arguments[1]; + service = arguments[2]; + action = arguments[3]; + actionArgs = arguments[4]; + + // Since we need to maintain backwards compatibility, we have to pass + // an invalid callbackId even if no callback was provided since plugins + // will be expecting it. The Cordova.exec() implementation allocates + // an invalid callbackId and passes it even if no callbacks were given. + callbackId = 'INVALID'; + } else { + throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' + + 'cordova.exec(null, null, \'Service\', \'action\', [ arg1, arg2 ]);' + ); + } + + // If actionArgs is not provided, default to an empty array + actionArgs = actionArgs || []; + + // Register the callbacks and add the callbackId to the positional + // arguments if given. + if (successCallback || failCallback) { + callbackId = service + cordova.callbackId++; + cordova.callbacks[callbackId] = + {success:successCallback, fail:failCallback}; + } + + actionArgs = massageArgsJsToNative(actionArgs); + + var command = [callbackId, service, action, actionArgs]; + + // Stringify and queue the command. We stringify to command now to + // effectively clone the command arguments in case they are mutated before + // the command is executed. + commandQueue.push(JSON.stringify(command)); + + // If we're in the context of a stringByEvaluatingJavaScriptFromString call, + // then the queue will be flushed when it returns; no need for a poke. + // Also, if there is already a command in the queue, then we've already + // poked the native side, so there is no reason to do so again. + if (!isInContextOfEvalJs && commandQueue.length == 1) { + pokeNative(); + } +} + +// CB-10530 +function proxyChanged() { + var cexec = cordovaExec(); + + return (execProxy !== cexec && // proxy objects are different + iOSExec !== cexec // proxy object is not the current iOSExec + ); +} + +// CB-10106 +function handleBridgeChange() { + if (proxyChanged()) { + var commandString = commandQueue.shift(); + while(commandString) { + var command = JSON.parse(commandString); + var callbackId = command[0]; + var service = command[1]; + var action = command[2]; + var actionArgs = command[3]; + var callbacks = cordova.callbacks[callbackId] || {}; + + execProxy(callbacks.success, callbacks.fail, service, action, actionArgs); + + commandString = commandQueue.shift(); + }; + return true; + } + + return false; +} + +function pokeNative() { + // CB-5488 - Don't attempt to create iframe before document.body is available. + if (!document.body) { + setTimeout(pokeNative); + return; + } + + // Check if they've removed it from the DOM, and put it back if so. + if (execIframe && execIframe.contentWindow) { + execIframe.contentWindow.location = 'gap://ready'; + } else { + execIframe = document.createElement('iframe'); + execIframe.style.display = 'none'; + execIframe.src = 'gap://ready'; + document.body.appendChild(execIframe); + } + // Use a timer to protect against iframe being unloaded during the poke (CB-7735). + // This makes the bridge ~ 7% slower, but works around the poke getting lost + // when the iframe is removed from the DOM. + // An onunload listener could be used in the case where the iframe has just been + // created, but since unload events fire only once, it doesn't work in the normal + // case of iframe reuse (where unload will have already fired due to the attempted + // navigation of the page). + failSafeTimerId = setTimeout(function() { + if (commandQueue.length) { + // CB-10106 - flush the queue on bridge change + if (!handleBridgeChange()) { + pokeNative(); + } + } + }, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire). +} + +iOSExec.nativeFetchMessages = function() { + // Stop listing for window detatch once native side confirms poke. + if (failSafeTimerId) { + clearTimeout(failSafeTimerId); + failSafeTimerId = 0; + } + // Each entry in commandQueue is a JSON string already. + if (!commandQueue.length) { + return ''; + } + var json = '[' + commandQueue.join(',') + ']'; + commandQueue.length = 0; + return json; +}; + +iOSExec.nativeCallback = function(callbackId, status, message, keepCallback, debug) { + return iOSExec.nativeEvalAndFetch(function() { + var success = status === 0 || status === 1; + var args = convertMessageToArgsNativeToJs(message); + function nc2() { + cordova.callbackFromNative(callbackId, success, status, args, keepCallback); + } + setTimeout(nc2, 0); + }); +}; + +iOSExec.nativeEvalAndFetch = function(func) { + // This shouldn't be nested, but better to be safe. + isInContextOfEvalJs++; + try { + func(); + return iOSExec.nativeFetchMessages(); + } finally { + isInContextOfEvalJs--; + } +}; + +// Proxy the exec for bridge changes. See CB-10106 + +function cordovaExec() { + var cexec = require('cordova/exec'); + var cexec_valid = (typeof cexec.nativeFetchMessages === 'function') && (typeof cexec.nativeEvalAndFetch === 'function') && (typeof cexec.nativeCallback === 'function'); + return (cexec_valid && execProxy !== cexec)? cexec : iOSExec; +} + +function execProxy() { + cordovaExec().apply(null, arguments); +}; + +execProxy.nativeFetchMessages = function() { + return cordovaExec().nativeFetchMessages.apply(null, arguments); +}; + +execProxy.nativeEvalAndFetch = function() { + return cordovaExec().nativeEvalAndFetch.apply(null, arguments); +}; + +execProxy.nativeCallback = function() { + return cordovaExec().nativeCallback.apply(null, arguments); +}; + +module.exports = execProxy; + +}); + +// file: src/common/exec/proxy.js +define("cordova/exec/proxy", function(require, exports, module) { + + +// internal map of proxy function +var CommandProxyMap = {}; + +module.exports = { + + // example: cordova.commandProxy.add("Accelerometer",{getCurrentAcceleration: function(successCallback, errorCallback, options) {...},...); + add:function(id,proxyObj) { + console.log("adding proxy for " + id); + CommandProxyMap[id] = proxyObj; + return proxyObj; + }, + + // cordova.commandProxy.remove("Accelerometer"); + remove:function(id) { + var proxy = CommandProxyMap[id]; + delete CommandProxyMap[id]; + CommandProxyMap[id] = null; + return proxy; + }, + + get:function(service,action) { + return ( CommandProxyMap[service] ? CommandProxyMap[service][action] : null ); + } +}; +}); + +// file: src/common/init.js +define("cordova/init", function(require, exports, module) { + +var channel = require('cordova/channel'); +var cordova = require('cordova'); +var modulemapper = require('cordova/modulemapper'); +var platform = require('cordova/platform'); +var pluginloader = require('cordova/pluginloader'); +var utils = require('cordova/utils'); + +var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady]; + +function logUnfiredChannels(arr) { + for (var i = 0; i < arr.length; ++i) { + if (arr[i].state != 2) { + console.log('Channel not fired: ' + arr[i].type); + } + } +} + +window.setTimeout(function() { + if (channel.onDeviceReady.state != 2) { + console.log('deviceready has not fired after 5 seconds.'); + logUnfiredChannels(platformInitChannelsArray); + logUnfiredChannels(channel.deviceReadyChannelsArray); + } +}, 5000); + +// Replace navigator before any modules are required(), to ensure it happens as soon as possible. +// We replace it so that properties that can't be clobbered can instead be overridden. +function replaceNavigator(origNavigator) { + var CordovaNavigator = function() {}; + CordovaNavigator.prototype = origNavigator; + var newNavigator = new CordovaNavigator(); + // This work-around really only applies to new APIs that are newer than Function.bind. + // Without it, APIs such as getGamepads() break. + if (CordovaNavigator.bind) { + for (var key in origNavigator) { + if (typeof origNavigator[key] == 'function') { + newNavigator[key] = origNavigator[key].bind(origNavigator); + } + else { + (function(k) { + utils.defineGetterSetter(newNavigator,key,function() { + return origNavigator[k]; + }); + })(key); + } + } + } + return newNavigator; +} + +if (window.navigator) { + window.navigator = replaceNavigator(window.navigator); +} + +if (!window.console) { + window.console = { + log: function(){} + }; +} +if (!window.console.warn) { + window.console.warn = function(msg) { + this.log("warn: " + msg); + }; +} + +// Register pause, resume and deviceready channels as events on document. +channel.onPause = cordova.addDocumentEventHandler('pause'); +channel.onResume = cordova.addDocumentEventHandler('resume'); +channel.onActivated = cordova.addDocumentEventHandler('activated'); +channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready'); + +// Listen for DOMContentLoaded and notify our channel subscribers. +if (document.readyState == 'complete' || document.readyState == 'interactive') { + channel.onDOMContentLoaded.fire(); +} else { + document.addEventListener('DOMContentLoaded', function() { + channel.onDOMContentLoaded.fire(); + }, false); +} + +// _nativeReady is global variable that the native side can set +// to signify that the native code is ready. It is a global since +// it may be called before any cordova JS is ready. +if (window._nativeReady) { + channel.onNativeReady.fire(); +} + +modulemapper.clobbers('cordova', 'cordova'); +modulemapper.clobbers('cordova/exec', 'cordova.exec'); +modulemapper.clobbers('cordova/exec', 'Cordova.exec'); + +// Call the platform-specific initialization. +platform.bootstrap && platform.bootstrap(); + +// Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js. +// The delay allows the attached modules to be defined before the plugin loader looks for them. +setTimeout(function() { + pluginloader.load(function() { + channel.onPluginsReady.fire(); + }); +}, 0); + +/** + * Create all cordova objects once native side is ready. + */ +channel.join(function() { + modulemapper.mapModules(window); + + platform.initialize && platform.initialize(); + + // Fire event to notify that all objects are created + channel.onCordovaReady.fire(); + + // Fire onDeviceReady event once page has fully loaded, all + // constructors have run and cordova info has been received from native + // side. + channel.join(function() { + require('cordova').fireDocumentEvent('deviceready'); + }, channel.deviceReadyChannelsArray); + +}, platformInitChannelsArray); + + +}); + +// file: src/common/init_b.js +define("cordova/init_b", function(require, exports, module) { + +var channel = require('cordova/channel'); +var cordova = require('cordova'); +var modulemapper = require('cordova/modulemapper'); +var platform = require('cordova/platform'); +var pluginloader = require('cordova/pluginloader'); +var utils = require('cordova/utils'); + +var platformInitChannelsArray = [channel.onDOMContentLoaded, channel.onNativeReady, channel.onPluginsReady]; + +// setting exec +cordova.exec = require('cordova/exec'); + +function logUnfiredChannels(arr) { + for (var i = 0; i < arr.length; ++i) { + if (arr[i].state != 2) { + console.log('Channel not fired: ' + arr[i].type); + } + } +} + +window.setTimeout(function() { + if (channel.onDeviceReady.state != 2) { + console.log('deviceready has not fired after 5 seconds.'); + logUnfiredChannels(platformInitChannelsArray); + logUnfiredChannels(channel.deviceReadyChannelsArray); + } +}, 5000); + +// Replace navigator before any modules are required(), to ensure it happens as soon as possible. +// We replace it so that properties that can't be clobbered can instead be overridden. +function replaceNavigator(origNavigator) { + var CordovaNavigator = function() {}; + CordovaNavigator.prototype = origNavigator; + var newNavigator = new CordovaNavigator(); + // This work-around really only applies to new APIs that are newer than Function.bind. + // Without it, APIs such as getGamepads() break. + if (CordovaNavigator.bind) { + for (var key in origNavigator) { + if (typeof origNavigator[key] == 'function') { + newNavigator[key] = origNavigator[key].bind(origNavigator); + } + else { + (function(k) { + utils.defineGetterSetter(newNavigator,key,function() { + return origNavigator[k]; + }); + })(key); + } + } + } + return newNavigator; +} +if (window.navigator) { + window.navigator = replaceNavigator(window.navigator); +} + +if (!window.console) { + window.console = { + log: function(){} + }; +} +if (!window.console.warn) { + window.console.warn = function(msg) { + this.log("warn: " + msg); + }; +} + +// Register pause, resume and deviceready channels as events on document. +channel.onPause = cordova.addDocumentEventHandler('pause'); +channel.onResume = cordova.addDocumentEventHandler('resume'); +channel.onActivated = cordova.addDocumentEventHandler('activated'); +channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready'); + +// Listen for DOMContentLoaded and notify our channel subscribers. +if (document.readyState == 'complete' || document.readyState == 'interactive') { + channel.onDOMContentLoaded.fire(); +} else { + document.addEventListener('DOMContentLoaded', function() { + channel.onDOMContentLoaded.fire(); + }, false); +} + +// _nativeReady is global variable that the native side can set +// to signify that the native code is ready. It is a global since +// it may be called before any cordova JS is ready. +if (window._nativeReady) { + channel.onNativeReady.fire(); +} + +// Call the platform-specific initialization. +platform.bootstrap && platform.bootstrap(); + +// Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js. +// The delay allows the attached modules to be defined before the plugin loader looks for them. +setTimeout(function() { + pluginloader.load(function() { + channel.onPluginsReady.fire(); + }); +}, 0); + +/** + * Create all cordova objects once native side is ready. + */ +channel.join(function() { + modulemapper.mapModules(window); + + platform.initialize && platform.initialize(); + + // Fire event to notify that all objects are created + channel.onCordovaReady.fire(); + + // Fire onDeviceReady event once page has fully loaded, all + // constructors have run and cordova info has been received from native + // side. + channel.join(function() { + require('cordova').fireDocumentEvent('deviceready'); + }, channel.deviceReadyChannelsArray); + +}, platformInitChannelsArray); + +}); + +// file: src/common/modulemapper.js +define("cordova/modulemapper", function(require, exports, module) { + +var builder = require('cordova/builder'), + moduleMap = define.moduleMap, + symbolList, + deprecationMap; + +exports.reset = function() { + symbolList = []; + deprecationMap = {}; +}; + +function addEntry(strategy, moduleName, symbolPath, opt_deprecationMessage) { + if (!(moduleName in moduleMap)) { + throw new Error('Module ' + moduleName + ' does not exist.'); + } + symbolList.push(strategy, moduleName, symbolPath); + if (opt_deprecationMessage) { + deprecationMap[symbolPath] = opt_deprecationMessage; + } +} + +// Note: Android 2.3 does have Function.bind(). +exports.clobbers = function(moduleName, symbolPath, opt_deprecationMessage) { + addEntry('c', moduleName, symbolPath, opt_deprecationMessage); +}; + +exports.merges = function(moduleName, symbolPath, opt_deprecationMessage) { + addEntry('m', moduleName, symbolPath, opt_deprecationMessage); +}; + +exports.defaults = function(moduleName, symbolPath, opt_deprecationMessage) { + addEntry('d', moduleName, symbolPath, opt_deprecationMessage); +}; + +exports.runs = function(moduleName) { + addEntry('r', moduleName, null); +}; + +function prepareNamespace(symbolPath, context) { + if (!symbolPath) { + return context; + } + var parts = symbolPath.split('.'); + var cur = context; + for (var i = 0, part; part = parts[i]; ++i) { + cur = cur[part] = cur[part] || {}; + } + return cur; +} + +exports.mapModules = function(context) { + var origSymbols = {}; + context.CDV_origSymbols = origSymbols; + for (var i = 0, len = symbolList.length; i < len; i += 3) { + var strategy = symbolList[i]; + var moduleName = symbolList[i + 1]; + var module = require(moduleName); + // + if (strategy == 'r') { + continue; + } + var symbolPath = symbolList[i + 2]; + var lastDot = symbolPath.lastIndexOf('.'); + var namespace = symbolPath.substr(0, lastDot); + var lastName = symbolPath.substr(lastDot + 1); + + var deprecationMsg = symbolPath in deprecationMap ? 'Access made to deprecated symbol: ' + symbolPath + '. ' + deprecationMsg : null; + var parentObj = prepareNamespace(namespace, context); + var target = parentObj[lastName]; + + if (strategy == 'm' && target) { + builder.recursiveMerge(target, module); + } else if ((strategy == 'd' && !target) || (strategy != 'd')) { + if (!(symbolPath in origSymbols)) { + origSymbols[symbolPath] = target; + } + builder.assignOrWrapInDeprecateGetter(parentObj, lastName, module, deprecationMsg); + } + } +}; + +exports.getOriginalSymbol = function(context, symbolPath) { + var origSymbols = context.CDV_origSymbols; + if (origSymbols && (symbolPath in origSymbols)) { + return origSymbols[symbolPath]; + } + var parts = symbolPath.split('.'); + var obj = context; + for (var i = 0; i < parts.length; ++i) { + obj = obj && obj[parts[i]]; + } + return obj; +}; + +exports.reset(); + + +}); + +// file: src/common/modulemapper_b.js +define("cordova/modulemapper_b", function(require, exports, module) { + +var builder = require('cordova/builder'), + symbolList = [], + deprecationMap; + +exports.reset = function() { + symbolList = []; + deprecationMap = {}; +}; + +function addEntry(strategy, moduleName, symbolPath, opt_deprecationMessage) { + symbolList.push(strategy, moduleName, symbolPath); + if (opt_deprecationMessage) { + deprecationMap[symbolPath] = opt_deprecationMessage; + } +} + +// Note: Android 2.3 does have Function.bind(). +exports.clobbers = function(moduleName, symbolPath, opt_deprecationMessage) { + addEntry('c', moduleName, symbolPath, opt_deprecationMessage); +}; + +exports.merges = function(moduleName, symbolPath, opt_deprecationMessage) { + addEntry('m', moduleName, symbolPath, opt_deprecationMessage); +}; + +exports.defaults = function(moduleName, symbolPath, opt_deprecationMessage) { + addEntry('d', moduleName, symbolPath, opt_deprecationMessage); +}; + +exports.runs = function(moduleName) { + addEntry('r', moduleName, null); +}; + +function prepareNamespace(symbolPath, context) { + if (!symbolPath) { + return context; + } + var parts = symbolPath.split('.'); + var cur = context; + for (var i = 0, part; part = parts[i]; ++i) { + cur = cur[part] = cur[part] || {}; + } + return cur; +} + +exports.mapModules = function(context) { + var origSymbols = {}; + context.CDV_origSymbols = origSymbols; + for (var i = 0, len = symbolList.length; i < len; i += 3) { + var strategy = symbolList[i]; + var moduleName = symbolList[i + 1]; + var module = require(moduleName); + // + if (strategy == 'r') { + continue; + } + var symbolPath = symbolList[i + 2]; + var lastDot = symbolPath.lastIndexOf('.'); + var namespace = symbolPath.substr(0, lastDot); + var lastName = symbolPath.substr(lastDot + 1); + + var deprecationMsg = symbolPath in deprecationMap ? 'Access made to deprecated symbol: ' + symbolPath + '. ' + deprecationMsg : null; + var parentObj = prepareNamespace(namespace, context); + var target = parentObj[lastName]; + + if (strategy == 'm' && target) { + builder.recursiveMerge(target, module); + } else if ((strategy == 'd' && !target) || (strategy != 'd')) { + if (!(symbolPath in origSymbols)) { + origSymbols[symbolPath] = target; + } + builder.assignOrWrapInDeprecateGetter(parentObj, lastName, module, deprecationMsg); + } + } +}; + +exports.getOriginalSymbol = function(context, symbolPath) { + var origSymbols = context.CDV_origSymbols; + if (origSymbols && (symbolPath in origSymbols)) { + return origSymbols[symbolPath]; + } + var parts = symbolPath.split('.'); + var obj = context; + for (var i = 0; i < parts.length; ++i) { + obj = obj && obj[parts[i]]; + } + return obj; +}; + +exports.reset(); + + +}); + +// file: /Users/ednamorales/dev/apache_plugins/cordova-ios/cordova-js-src/platform.js +define("cordova/platform", function(require, exports, module) { + +module.exports = { + id: 'ios', + bootstrap: function() { + require('cordova/channel').onNativeReady.fire(); + } +}; + + +}); + +// file: src/common/pluginloader.js +define("cordova/pluginloader", function(require, exports, module) { + +var modulemapper = require('cordova/modulemapper'); +var urlutil = require('cordova/urlutil'); + +// Helper function to inject a +
+ diff --git a/msext/Class/WebViewJavascriptBridge/ExampleWKWebViewController.h b/msext/Class/WebViewJavascriptBridge/ExampleWKWebViewController.h new file mode 100755 index 0000000..7dd92b8 --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/ExampleWKWebViewController.h @@ -0,0 +1,14 @@ +// +// ExampleWKWebViewController.h +// ExampleApp-iOS +// +// Created by Marcus Westin on 1/13/14. +// Copyright (c) 2014 Marcus Westin. All rights reserved. +// + +#import +#import + +@interface ExampleWKWebViewController : UINavigationController + +@end \ No newline at end of file diff --git a/msext/Class/WebViewJavascriptBridge/ExampleWKWebViewController.m b/msext/Class/WebViewJavascriptBridge/ExampleWKWebViewController.m new file mode 100755 index 0000000..26598e0 --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/ExampleWKWebViewController.m @@ -0,0 +1,85 @@ +// +// ExampleWKWebViewController.m +// ExampleApp-iOS +// +// Created by Marcus Westin on 1/13/14. +// Copyright (c) 2014 Marcus Westin. All rights reserved. +// + +#import "ExampleWKWebViewController.h" +#import "WebViewJavascriptBridge.h" + +@interface ExampleWKWebViewController () + +@property WebViewJavascriptBridge* bridge; + +@end + +@implementation ExampleWKWebViewController + +- (void)viewWillAppear:(BOOL)animated { + if (_bridge) { return; } + + WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds]; + webView.navigationDelegate = self; + [self.view addSubview:webView]; + [WebViewJavascriptBridge enableLogging]; + _bridge = [WebViewJavascriptBridge bridgeForWebView:webView]; + [_bridge setWebViewDelegate:self]; + + [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) { + NSLog(@"testObjcCallback called: %@", data); + responseCallback(@"Response from testObjcCallback"); + }]; + + [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }]; + + [self renderButtons:webView]; + [self loadExamplePage:webView]; +} + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + NSLog(@"webViewDidStartLoad"); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + NSLog(@"webViewDidFinishLoad"); +} + +- (void)renderButtons:(WKWebView*)webView { + UIFont* font = [UIFont fontWithName:@"HelveticaNeue" size:12.0]; + + UIButton *callbackButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; + [callbackButton setTitle:@"Call handler" forState:UIControlStateNormal]; + [callbackButton addTarget:self action:@selector(callHandler:) forControlEvents:UIControlEventTouchUpInside]; + [self.view insertSubview:callbackButton aboveSubview:webView]; + callbackButton.frame = CGRectMake(10, 400, 100, 35); + callbackButton.titleLabel.font = font; + + UIButton* reloadButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; + [reloadButton setTitle:@"Reload webview" forState:UIControlStateNormal]; + [reloadButton addTarget:webView action:@selector(reload) forControlEvents:UIControlEventTouchUpInside]; + [self.view insertSubview:reloadButton aboveSubview:webView]; + reloadButton.frame = CGRectMake(110, 400, 100, 35); + reloadButton.titleLabel.font = font; +} + +- (void)callHandler:(id)sender { + id data = @{ @"greetingFromObjC": @"Hi there, JS!" }; + [_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) { + NSLog(@"testJavascriptHandler responded: %@", response); + }]; +} + +- (void)loadExamplePage:(WKWebView*)webView { + NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; + NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil]; + NSURL *baseURL = [NSURL fileURLWithPath:htmlPath]; + [webView loadHTMLString:[NSString stringWithFormat:@"%@",appHtml] baseURL:baseURL]; + +// NSString *htmlPath=[FuncPublic getFilePath:[NSString stringWithFormat:@"%@/%@/index.html",[FuncPublic filename:@"gamedir"],[FuncPublic filename:@"gamestart"]] PathType:2]; +// NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil]; +// NSURL *baseURL =[NSURL fileURLWithPath:[FuncPublic getFilePath:[NSString stringWithFormat:@"%@/%@",[FuncPublic filename:@"gamedir"],[FuncPublic filename:@"gamestart"]] PathType:2]]; +// [webView loadHTMLString:[NSString stringWithFormat:@"%@",appHtml] baseURL:baseURL]; +} +@end diff --git a/msext/Class/WebViewJavascriptBridge/WKWebViewJavascriptBridge.h b/msext/Class/WebViewJavascriptBridge/WKWebViewJavascriptBridge.h new file mode 100755 index 0000000..4e3404f --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WKWebViewJavascriptBridge.h @@ -0,0 +1,34 @@ +// +// WKWebViewJavascriptBridge.h +// +// Created by @LokiMeyburg on 10/15/14. +// Copyright (c) 2014 @LokiMeyburg. All rights reserved. +// + +#if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_1) +#define supportsWKWebView +#endif + +#if defined supportsWKWebView + +#import +#import "WebViewJavascriptBridgeBase.h" +#import + +@interface WKWebViewJavascriptBridge : NSObject + ++ (instancetype)bridgeForWebView:(WKWebView*)webView; ++ (void)enableLogging; + +- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; +- (void)removeHandler:(NSString*)handlerName; +- (void)callHandler:(NSString*)handlerName; +- (void)callHandler:(NSString*)handlerName data:(id)data; +- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; +- (void)reset; +- (void)setWebViewDelegate:(id)webViewDelegate; +- (void)disableJavscriptAlertBoxSafetyTimeout; + +@end + +#endif diff --git a/msext/Class/WebViewJavascriptBridge/WKWebViewJavascriptBridge.m b/msext/Class/WebViewJavascriptBridge/WKWebViewJavascriptBridge.m new file mode 100755 index 0000000..73c923d --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WKWebViewJavascriptBridge.m @@ -0,0 +1,198 @@ +// +// WKWebViewJavascriptBridge.m +// +// Created by @LokiMeyburg on 10/15/14. +// Copyright (c) 2014 @LokiMeyburg. All rights reserved. +// + + +#import "WKWebViewJavascriptBridge.h" + +#if defined supportsWKWebView + +@implementation WKWebViewJavascriptBridge { + __weak WKWebView* _webView; + __weak id _webViewDelegate; + long _uniqueId; + WebViewJavascriptBridgeBase *_base; +} + +/* API + *****/ + ++ (void)enableLogging { [WebViewJavascriptBridgeBase enableLogging]; } + ++ (instancetype)bridgeForWebView:(WKWebView*)webView { + WKWebViewJavascriptBridge* bridge = [[self alloc] init]; + [bridge _setupInstance:webView]; + [bridge reset]; + return bridge; +} + +- (void)send:(id)data { + [self send:data responseCallback:nil]; +} + +- (void)send:(id)data responseCallback:(WVJBResponseCallback)responseCallback { + [_base sendData:data responseCallback:responseCallback handlerName:nil]; +} + +- (void)callHandler:(NSString *)handlerName { + [self callHandler:handlerName data:nil responseCallback:nil]; +} + +- (void)callHandler:(NSString *)handlerName data:(id)data { + [self callHandler:handlerName data:data responseCallback:nil]; +} + +- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback { + [_base sendData:data responseCallback:responseCallback handlerName:handlerName]; +} + +- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { + _base.messageHandlers[handlerName] = [handler copy]; +} + +- (void)removeHandler:(NSString *)handlerName { + [_base.messageHandlers removeObjectForKey:handlerName]; +} + +- (void)reset { + [_base reset]; +} + +- (void)setWebViewDelegate:(id)webViewDelegate { + _webViewDelegate = webViewDelegate; +} + +- (void)disableJavscriptAlertBoxSafetyTimeout { + [_base disableJavscriptAlertBoxSafetyTimeout]; +} + +/* Internals + ***********/ + +- (void)dealloc { + _base = nil; + _webView = nil; + _webViewDelegate = nil; + _webView.navigationDelegate = nil; +} + + +/* WKWebView Specific Internals + ******************************/ + +- (void) _setupInstance:(WKWebView*)webView { + _webView = webView; + _webView.navigationDelegate = self; + _base = [[WebViewJavascriptBridgeBase alloc] init]; + _base.delegate = self; +} + + +- (void)WKFlushMessageQueue { + [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) { + if (error != nil) { + NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error); + } + [_base flushMessageQueue:result]; + }]; +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + if (webView != _webView) { return; } + + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFinishNavigation:)]) { + [strongDelegate webView:webView didFinishNavigation:navigation]; + } +} + + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + if (webView != _webView) { return; } + + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationResponse:decisionHandler:)]) { + [strongDelegate webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler]; + } + else { + decisionHandler(WKNavigationResponsePolicyAllow); + } +} + +- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { + if (webView != _webView) { return; } + + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didReceiveAuthenticationChallenge:completionHandler:)]) { + [strongDelegate webView:webView didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler]; + } else { + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + } +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + if (webView != _webView) { return; } + NSURL *url = navigationAction.request.URL; + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + + if ([_base isWebViewJavascriptBridgeURL:url]) { + if ([_base isBridgeLoadedURL:url]) { + [_base injectJavascriptFile]; + } else if ([_base isQueueMessageURL:url]) { + [self WKFlushMessageQueue]; + } else { + [_base logUnkownMessage:url]; + } + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) { + [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; + } else { + decisionHandler(WKNavigationActionPolicyAllow); + } +} + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + if (webView != _webView) { return; } + + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didStartProvisionalNavigation:)]) { + [strongDelegate webView:webView didStartProvisionalNavigation:navigation]; + } +} + + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { + if (webView != _webView) { return; } + + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFailNavigation:withError:)]) { + [strongDelegate webView:webView didFailNavigation:navigation withError:error]; + } +} + +- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { + if (webView != _webView) { return; } + + __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFailProvisionalNavigation:withError:)]) { + [strongDelegate webView:webView didFailProvisionalNavigation:navigation withError:error]; + } +} + +- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand { + [_webView evaluateJavaScript:javascriptCommand completionHandler:nil]; + return NULL; +} + + + +@end + + +#endif diff --git a/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge.h b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge.h new file mode 100755 index 0000000..1b64bb4 --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge.h @@ -0,0 +1,50 @@ +// +// WebViewJavascriptBridge.h +// ExampleApp-iOS +// +// Created by Marcus Westin on 6/14/13. +// Copyright (c) 2013 Marcus Westin. All rights reserved. +// + +#import +#import "WebViewJavascriptBridgeBase.h" + +#if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_1) +#define supportsWKWebView +#endif + +#if defined supportsWKWebView +#import +#endif + +#if defined __MAC_OS_X_VERSION_MAX_ALLOWED + #define WVJB_PLATFORM_OSX + #define WVJB_WEBVIEW_TYPE WebView + #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject + #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject +#elif defined __IPHONE_OS_VERSION_MAX_ALLOWED + #import + #define WVJB_PLATFORM_IOS + #define WVJB_WEBVIEW_TYPE UIWebView + #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject + #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject +#endif + +@interface WebViewJavascriptBridge : WVJB_WEBVIEW_DELEGATE_INTERFACE + + ++ (instancetype)bridgeForWebView:(id)webView; ++ (instancetype)bridge:(id)webView; + ++ (void)enableLogging; ++ (void)setLogMaxLength:(int)length; + +- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; +- (void)removeHandler:(NSString*)handlerName; +- (void)callHandler:(NSString*)handlerName; +- (void)callHandler:(NSString*)handlerName data:(id)data; +- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; +- (void)setWebViewDelegate:(id)webViewDelegate; +- (void)disableJavscriptAlertBoxSafetyTimeout; + +@end diff --git a/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge.m b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge.m new file mode 100755 index 0000000..e74a6e2 --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge.m @@ -0,0 +1,211 @@ +// +// WebViewJavascriptBridge.m +// ExampleApp-iOS +// +// Created by Marcus Westin on 6/14/13. +// Copyright (c) 2013 Marcus Westin. All rights reserved. +// + +#import "WebViewJavascriptBridge.h" + +#if defined(supportsWKWebView) +#import "WKWebViewJavascriptBridge.h" +#endif + +#if __has_feature(objc_arc_weak) + #define WVJB_WEAK __weak +#else + #define WVJB_WEAK __unsafe_unretained +#endif + +@implementation WebViewJavascriptBridge { + WVJB_WEAK WVJB_WEBVIEW_TYPE* _webView; + WVJB_WEAK id _webViewDelegate; + long _uniqueId; + WebViewJavascriptBridgeBase *_base; +} + +/* API + *****/ + ++ (void)enableLogging { + [WebViewJavascriptBridgeBase enableLogging]; +} ++ (void)setLogMaxLength:(int)length { + [WebViewJavascriptBridgeBase setLogMaxLength:length]; +} + ++ (instancetype)bridgeForWebView:(id)webView { + return [self bridge:webView]; +} ++ (instancetype)bridge:(id)webView { +#if defined supportsWKWebView + if ([webView isKindOfClass:[WKWebView class]]) { + return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView]; + } +#endif + if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) { + WebViewJavascriptBridge* bridge = [[self alloc] init]; + [bridge _platformSpecificSetup:webView]; + return bridge; + } + [NSException raise:@"BadWebViewType" format:@"Unknown web view type."]; + return nil; +} + +- (void)setWebViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate { + _webViewDelegate = webViewDelegate; +} + +- (void)send:(id)data { + [self send:data responseCallback:nil]; +} + +- (void)send:(id)data responseCallback:(WVJBResponseCallback)responseCallback { + [_base sendData:data responseCallback:responseCallback handlerName:nil]; +} + +- (void)callHandler:(NSString *)handlerName { + [self callHandler:handlerName data:nil responseCallback:nil]; +} + +- (void)callHandler:(NSString *)handlerName data:(id)data { + [self callHandler:handlerName data:data responseCallback:nil]; +} + +- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback { + [_base sendData:data responseCallback:responseCallback handlerName:handlerName]; +} + +- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { + _base.messageHandlers[handlerName] = [handler copy]; +} + +- (void)removeHandler:(NSString *)handlerName { + [_base.messageHandlers removeObjectForKey:handlerName]; +} + +- (void)disableJavscriptAlertBoxSafetyTimeout { + [_base disableJavscriptAlertBoxSafetyTimeout]; +} + + +/* Platform agnostic internals + *****************************/ + +- (void)dealloc { + [self _platformSpecificDealloc]; + _base = nil; + _webView = nil; + _webViewDelegate = nil; +} + +- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand { + return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand]; +} + +#if defined WVJB_PLATFORM_OSX +/* Platform specific internals: OSX + **********************************/ + +- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView { + _webView = webView; + _webView.policyDelegate = self; + _base = [[WebViewJavascriptBridgeBase alloc] init]; + _base.delegate = self; +} + +- (void) _platformSpecificDealloc { + _webView.policyDelegate = nil; +} + +- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { + if (webView != _webView) { return; } + + NSURL *url = [request URL]; + if ([_base isWebViewJavascriptBridgeURL:url]) { + if ([_base isBridgeLoadedURL:url]) { + [_base injectJavascriptFile]; + } else if ([_base isQueueMessageURL:url]) { + NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; + [_base flushMessageQueue:messageQueueString]; + } else { + [_base logUnkownMessage:url]; + } + [listener ignore]; + } else if (_webViewDelegate && [_webViewDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:request:frame:decisionListener:)]) { + [_webViewDelegate webView:webView decidePolicyForNavigationAction:actionInformation request:request frame:frame decisionListener:listener]; + } else { + [listener use]; + } +} + + + +#elif defined WVJB_PLATFORM_IOS +/* Platform specific internals: iOS + **********************************/ + +- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView { + _webView = webView; + _webView.delegate = self; + _base = [[WebViewJavascriptBridgeBase alloc] init]; + _base.delegate = self; +} + +- (void) _platformSpecificDealloc { + _webView.delegate = nil; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + if (webView != _webView) { return; } + + __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { + [strongDelegate webViewDidFinishLoad:webView]; + } +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + if (webView != _webView) { return; } + + __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { + [strongDelegate webView:webView didFailLoadWithError:error]; + } +} + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + if (webView != _webView) { return YES; } + + NSURL *url = [request URL]; + __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate; + if ([_base isWebViewJavascriptBridgeURL:url]) { + if ([_base isBridgeLoadedURL:url]) { + [_base injectJavascriptFile]; + } else if ([_base isQueueMessageURL:url]) { + NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; + [_base flushMessageQueue:messageQueueString]; + } else { + [_base logUnkownMessage:url]; + } + return NO; + } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { + return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; + } else { + return YES; + } +} + +- (void)webViewDidStartLoad:(UIWebView *)webView { + if (webView != _webView) { return; } + + __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [strongDelegate webViewDidStartLoad:webView]; + } +} + +#endif + +@end diff --git a/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.h b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.h new file mode 100755 index 0000000..54d80ac --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.h @@ -0,0 +1,46 @@ +// +// WebViewJavascriptBridgeBase.h +// +// Created by @LokiMeyburg on 10/15/14. +// Copyright (c) 2014 @LokiMeyburg. All rights reserved. +// + +#import + +#define kOldProtocolScheme @"wvjbscheme" +#define kNewProtocolScheme @"https" +#define kQueueHasMessage @"__wvjb_queue_message__" +#define kBridgeLoaded @"__bridge_loaded__" + +typedef void (^WVJBResponseCallback)(id responseData); +typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback); +typedef NSDictionary WVJBMessage; + +@protocol WebViewJavascriptBridgeBaseDelegate +- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand; +@end + +@interface WebViewJavascriptBridgeBase : NSObject + + +@property (weak, nonatomic) id delegate; +@property (strong, nonatomic) NSMutableArray* startupMessageQueue; +@property (strong, nonatomic) NSMutableDictionary* responseCallbacks; +@property (strong, nonatomic) NSMutableDictionary* messageHandlers; +@property (strong, nonatomic) WVJBHandler messageHandler; + ++ (void)enableLogging; ++ (void)setLogMaxLength:(int)length; +- (void)reset; +- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName; +- (void)flushMessageQueue:(NSString *)messageQueueString; +- (void)injectJavascriptFile; +- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url; +- (BOOL)isQueueMessageURL:(NSURL*)urll; +- (BOOL)isBridgeLoadedURL:(NSURL*)urll; +- (void)logUnkownMessage:(NSURL*)url; +- (NSString *)webViewJavascriptCheckCommand; +- (NSString *)webViewJavascriptFetchQueyCommand; +- (void)disableJavscriptAlertBoxSafetyTimeout; + +@end diff --git a/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.m b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.m new file mode 100755 index 0000000..3ec26ed --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.m @@ -0,0 +1,221 @@ +// +// WebViewJavascriptBridgeBase.m +// +// Created by @LokiMeyburg on 10/15/14. +// Copyright (c) 2014 @LokiMeyburg. All rights reserved. +// + +#import +#import "WebViewJavascriptBridgeBase.h" +#import "WebViewJavascriptBridge_JS.h" + +@implementation WebViewJavascriptBridgeBase { + __weak id _webViewDelegate; + long _uniqueId; +} + +static bool logging = false; +static int logMaxLength = 500; + ++ (void)enableLogging { logging = true; } ++ (void)setLogMaxLength:(int)length { logMaxLength = length;} + +- (id)init { + if (self = [super init]) { + self.messageHandlers = [NSMutableDictionary dictionary]; + self.startupMessageQueue = [NSMutableArray array]; + self.responseCallbacks = [NSMutableDictionary dictionary]; + _uniqueId = 0; + } + return self; +} + +- (void)dealloc { + self.startupMessageQueue = nil; + self.responseCallbacks = nil; + self.messageHandlers = nil; +} + +- (void)reset { + self.startupMessageQueue = [NSMutableArray array]; + self.responseCallbacks = [NSMutableDictionary dictionary]; + _uniqueId = 0; +} + +- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName { + NSMutableDictionary* message = [NSMutableDictionary dictionary]; + + if (data) { + message[@"data"] = data; + } + + if (responseCallback) { + NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId]; + self.responseCallbacks[callbackId] = [responseCallback copy]; + message[@"callbackId"] = callbackId; + } + + if (handlerName) { + message[@"handlerName"] = handlerName; + } + [self _queueMessage:message]; +} + +- (void)flushMessageQueue:(NSString *)messageQueueString{ + if (messageQueueString == nil || messageQueueString.length == 0) { + NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page."); + return; + } + + id messages = [self _deserializeMessageJSON:messageQueueString]; + for (WVJBMessage* message in messages) { + if (![message isKindOfClass:[WVJBMessage class]]) { + NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message); + continue; + } + [self _log:@"RCVD" json:message]; + + NSString* responseId = message[@"responseId"]; + if (responseId) { + WVJBResponseCallback responseCallback = _responseCallbacks[responseId]; + responseCallback(message[@"responseData"]); + [self.responseCallbacks removeObjectForKey:responseId]; + } else { + WVJBResponseCallback responseCallback = NULL; + NSString* callbackId = message[@"callbackId"]; + if (callbackId) { + responseCallback = ^(id responseData) { + if (responseData == nil) { + responseData = [NSNull null]; + } + + WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData }; + [self _queueMessage:msg]; + }; + } else { + responseCallback = ^(id ignoreResponseData) { + // Do nothing + }; + } + + WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; + + if (!handler) { + NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message); + continue; + } + + handler(message[@"data"], responseCallback); + } + } +} + +- (void)injectJavascriptFile { + NSString *js = WebViewJavascriptBridge_js(); + [self _evaluateJavascript:js]; + if (self.startupMessageQueue) { + NSArray* queue = self.startupMessageQueue; + self.startupMessageQueue = nil; + for (id queuedMessage in queue) { + [self _dispatchMessage:queuedMessage]; + } + } +} + +- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url { + if (![self isSchemeMatch:url]) { + return NO; + } + return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url]; +} + +- (BOOL)isSchemeMatch:(NSURL*)url { + NSString* scheme = url.scheme.lowercaseString; + return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme]; +} + +- (BOOL)isQueueMessageURL:(NSURL*)url { + NSString* host = url.host.lowercaseString; + return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage]; +} + +- (BOOL)isBridgeLoadedURL:(NSURL*)url { + NSString* host = url.host.lowercaseString; + return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded]; +} + +- (void)logUnkownMessage:(NSURL*)url { + NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@", [url absoluteString]); +} + +- (NSString *)webViewJavascriptCheckCommand { + return @"typeof WebViewJavascriptBridge == \'object\';"; +} + +- (NSString *)webViewJavascriptFetchQueyCommand { + return @"WebViewJavascriptBridge._fetchQueue();"; +} + +- (void)disableJavscriptAlertBoxSafetyTimeout { + [self sendData:nil responseCallback:nil handlerName:@"_disableJavascriptAlertBoxSafetyTimeout"]; +} + +// Private +// ------------------------------------------- + +- (void) _evaluateJavascript:(NSString *)javascriptCommand { + [self.delegate _evaluateJavascript:javascriptCommand]; +} + +- (void)_queueMessage:(WVJBMessage*)message { + if (self.startupMessageQueue) { + [self.startupMessageQueue addObject:message]; + } else { + [self _dispatchMessage:message]; + } +} + +- (void)_dispatchMessage:(WVJBMessage*)message { + NSString *messageJSON = [self _serializeMessage:message pretty:NO]; + [self _log:@"SEND" json:messageJSON]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"]; + messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"]; + + NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON]; + if ([[NSThread currentThread] isMainThread]) { + [self _evaluateJavascript:javascriptCommand]; + + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self _evaluateJavascript:javascriptCommand]; + }); + } +} + +- (NSString *)_serializeMessage:(id)message pretty:(BOOL)pretty{ + return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(pretty ? NSJSONWritingPrettyPrinted : 0) error:nil] encoding:NSUTF8StringEncoding]; +} + +- (NSArray*)_deserializeMessageJSON:(NSString *)messageJSON { + return [NSJSONSerialization JSONObjectWithData:[messageJSON dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil]; +} + +- (void)_log:(NSString *)action json:(id)json { + if (!logging) { return; } + if (![json isKindOfClass:[NSString class]]) { + json = [self _serializeMessage:json pretty:YES]; + } + if ([json length] > logMaxLength) { + NSLog(@"WVJB %@: %@ [...]", action, [json substringToIndex:logMaxLength]); + } else { + NSLog(@"WVJB %@: %@", action, json); + } +} + +@end diff --git a/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge_JS.h b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge_JS.h new file mode 100755 index 0000000..6cb1cb9 --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge_JS.h @@ -0,0 +1,3 @@ +#import + +NSString * WebViewJavascriptBridge_js(); \ No newline at end of file diff --git a/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge_JS.m b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge_JS.m new file mode 100755 index 0000000..670a552 --- /dev/null +++ b/msext/Class/WebViewJavascriptBridge/WebViewJavascriptBridge_JS.m @@ -0,0 +1,139 @@ +// This file contains the source for the Javascript side of the +// WebViewJavascriptBridge. It is plaintext, but converted to an NSString +// via some preprocessor tricks. +// +// Previous implementations of WebViewJavascriptBridge loaded the javascript source +// from a resource. This worked fine for app developers, but library developers who +// included the bridge into their library, awkwardly had to ask consumers of their +// library to include the resource, violating their encapsulation. By including the +// Javascript as a string resource, the encapsulation of the library is maintained. + +#import "WebViewJavascriptBridge_JS.h" + +NSString * WebViewJavascriptBridge_js() { + #define __wvjb_js_func__(x) #x + + // BEGIN preprocessorJSCode + static NSString * preprocessorJSCode = @__wvjb_js_func__( +;(function() { + if (window.WebViewJavascriptBridge) { + return; + } + + if (!window.onerror) { + window.onerror = function(msg, url, line) { + console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line); + } + } + window.WebViewJavascriptBridge = { + registerHandler: registerHandler, + callHandler: callHandler, + disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, + _fetchQueue: _fetchQueue, + _handleMessageFromObjC: _handleMessageFromObjC + }; + + var messagingIframe; + var sendMessageQueue = []; + var messageHandlers = {}; + + var CUSTOM_PROTOCOL_SCHEME = 'https'; + var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; + + var responseCallbacks = {}; + var uniqueId = 1; + var dispatchMessagesWithTimeoutSafety = true; + + function registerHandler(handlerName, handler) { + messageHandlers[handlerName] = handler; + } + + function callHandler(handlerName, data, responseCallback) { + if (arguments.length == 2 && typeof data == 'function') { + responseCallback = data; + data = null; + } + _doSend({ handlerName:handlerName, data:data }, responseCallback); + } + function disableJavscriptAlertBoxSafetyTimeout() { + dispatchMessagesWithTimeoutSafety = false; + } + + function _doSend(message, responseCallback) { + if (responseCallback) { + var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); + responseCallbacks[callbackId] = responseCallback; + message['callbackId'] = callbackId; + } + sendMessageQueue.push(message); + messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; + } + + function _fetchQueue() { + var messageQueueString = JSON.stringify(sendMessageQueue); + sendMessageQueue = []; + return messageQueueString; + } + + function _dispatchMessageFromObjC(messageJSON) { + if (dispatchMessagesWithTimeoutSafety) { + setTimeout(_doDispatchMessageFromObjC); + } else { + _doDispatchMessageFromObjC(); + } + + function _doDispatchMessageFromObjC() { + var message = JSON.parse(messageJSON); + var messageHandler; + var responseCallback; + + if (message.responseId) { + responseCallback = responseCallbacks[message.responseId]; + if (!responseCallback) { + return; + } + responseCallback(message.responseData); + delete responseCallbacks[message.responseId]; + } else { + if (message.callbackId) { + var callbackResponseId = message.callbackId; + responseCallback = function(responseData) { + _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }); + }; + } + + var handler = messageHandlers[message.handlerName]; + if (!handler) { + console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); + } else { + handler(message.data, responseCallback); + } + } + } + } + + function _handleMessageFromObjC(messageJSON) { + _dispatchMessageFromObjC(messageJSON); + } + + messagingIframe = document.createElement('iframe'); + messagingIframe.style.display = 'none'; + messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; + document.documentElement.appendChild(messagingIframe); + + registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout); + + setTimeout(_callWVJBCallbacks, 0); + function _callWVJBCallbacks() { + var callbacks = window.WVJBCallbacks; + delete window.WVJBCallbacks; + for (var i=0; i +#import "XianliaoApiObject.h" + +@interface XianliaoApiManager : NSObject + + +/** + 设置出错的时候是否显示信息,默认是false,会直接跳过,在调试的时候应该打开。 + */ ++ (void)showLog:(BOOL)show; + +/** + 判断是否安装了闲聊 + */ ++ (BOOL)isInstallXianliao; + +/** + 获得闲聊Api的版本号 + */ ++ (NSString *)getApiVersion; + +/* + 向闲聊终端程序注册第三方应用,此方法只能执行一次。 + */ ++ (void)registerApp:(NSString *)appid; + +/** + 用户从闲聊调用你的APP时,需要从这个方法获得闲聊传递的内容。 + 需要在AppDelegate的两个方法中执行 + (iOS 9或以上) application:openURL:options: + (iOS 9以下) application:openURL:sourceApplication:annotation: + + @param url AppDelegate方法中的url + @return 是否是Xianliao的调用 + */ ++ (BOOL)handleOpenURL:(NSURL *)url; + +/** + 分享接口 + + @param object 分享的对象 + @param callBackBlock 分享结束后用户返回APP时会执行的回调block + */ ++ (void)share:(XianliaoShareBaseObject *)object fininshBlock:(XianliaoShareCallBackBlock)callBackBlock; + +/** + 登录接口 + + @param state 用于保持请求和回调的状态,授权请求后原样带回给第三方。 + @param callBackBlock 分享结束后用户返回APP时会执行的回调block + */ ++ (void)loginState:(NSString *)state fininshBlock:(XianliaoLoginCallBackBlock)callBackBlock; + + +/** + 注册从闲聊跳转过来的应用调用,注册了以后从闲聊调起你的APP时会以下的block,如果重复执行这个方法,会执行最后注册的block + */ ++ (void)getAppFromXianliao:(XianliaoAppBlock)block; + + +@end diff --git a/msext/Class/XiaoliaoSDK_iOS/XianliaoApiObject.h b/msext/Class/XiaoliaoSDK_iOS/XianliaoApiObject.h new file mode 100755 index 0000000..8560f3f --- /dev/null +++ b/msext/Class/XiaoliaoSDK_iOS/XianliaoApiObject.h @@ -0,0 +1,147 @@ +// +// XianliaoApiObject.h +// XianliaoApi +// +// Created by bu88 on 2017/3/15. +// Copyright © 2017年 HHJ. All rights reserved. +// + +#import + +#pragma mark:-----闲聊Api分享和登录的基础部分---- +/** + 闲聊分享和登录对象基类 + */ +@interface XianliaoApiObject : NSObject + +@end + + +#pragma mark:-----分享----- +/** + 分享回调的情景 + + - XianliaoShareSuccesslType: 分享成功 + - XianliaoShareCancelType: 分享取消 + - XianliaoShareErrorType: 分享失败 + - XianliaoShareUnkonwType: 未知 + */ +typedef NS_ENUM(NSInteger, XianliaoShareCallBackType) { + XianliaoShareSuccesslType = 0, + XianliaoShareCancelType, + XianliaoShareErrorType, + XianliaoShareUnkonwType, +}; +/// 分享的回调block +typedef void (^XianliaoShareCallBackBlock)(XianliaoShareCallBackType callBackType); + + +/** + 分享类型 + + - XianliaoShareTextObjectType: 文本分享类型 + - XianliaoShareImageObjectType: 图片分享类型 + - XianliaoShareAppObjectType: 应用分享类型 + - XianliaoShareLinkObjectType: 链接分享类型 + */ +typedef NS_ENUM(NSInteger, XianliaoShareObjectType) { + XianliaoShareTextObjectType = 0, + XianliaoShareImageObjectType, + XianliaoShareAppObjectType, + XianliaoShareLinkObjectType = 10, +}; + + +/** + 闲聊分享基类 + */ +@interface XianliaoShareBaseObject : XianliaoApiObject +/// 分享类型 +@property(nonatomic, assign, readonly) XianliaoShareObjectType type; + +@end + + +/** + 文本类型的分型,文本分享必须传分享内容(如果不传分享内容则无法分享) + */ +@interface XianliaoShareTextObject : XianliaoShareBaseObject +/// 分享内容 +@property(nonatomic, copy) NSString *text; + +@end + + +/** + 图片类型的分型,图片分享必须传分享图片URL + */ +@interface XianliaoShareImageObject : XianliaoShareBaseObject +/// 分享图片URL +@property(nonatomic, copy) NSString *imageUrl; +/// 分享的图片本身 +@property(nonatomic, strong) NSData *imageData; + +@end + + +/** + 应用分享类型,应用分享必须传递应用标题,应用描述和应用缩略图 + */ +@interface XianliaoShareAppObject : XianliaoShareBaseObject +/// 应用房间号 +@property(nonatomic, copy) NSString *roomToken; +/// 应用房间标识 +@property(nonatomic, copy) NSString *roomId; +/// 应用标题 +@property(nonatomic, copy) NSString *title; +/// 应用描述 +@property(nonatomic, copy) NSString *text; +/// 应用缩略图URL +@property(nonatomic, copy) NSString *imageUrl; +/// 应用缩略图本身 +@property(nonatomic, strong) NSData *imageData; +/// 安卓下載地址 +@property (nonatomic, copy) NSString *androidDownloadUrl; +/// iOS下載地址 +@property (nonatomic, copy) NSString *iOSDownloadUrl; + +@end + +@interface XianliaoShareLinkObject : XianliaoShareBaseObject + +/// 链接标题 +@property(nonatomic, copy) NSString *title; +/// 链接描述 +@property(nonatomic, copy) NSString *linkDescription; +/// 链接缩略图URL +@property(nonatomic, copy) NSString *imageUrl; +/// 链接缩略图本身 +@property(nonatomic, strong) NSData *imageData; +/// 链接 +@property(nonatomic, copy) NSString *url; + +@end + + +#pragma mark:-----登录----- +/** + 登录的回调场景 + + - XianliaoLoginSuccessType: 登录成功 + - XianliaoLoginCancelType: 登录取消 + - XianliaoLoginErrorType: 登录错误 + - XianliaoLoginUnkonwType: 未知 + */ +typedef NS_ENUM(NSInteger, XianliaoLoginCallBackType) { + XianliaoLoginSuccessType = 0, + XianliaoLoginCancelType, + XianliaoLoginErrorType, + XianliaoLoginUnkonwType, +}; +/// 登录的回调block +typedef void (^XianliaoLoginCallBackBlock)(XianliaoLoginCallBackType callBackType, NSString *code, NSString *state); + + +#pragma mark:-----应用----- +/// 登录的调用block +typedef void (^XianliaoAppBlock)(NSString *roomToken, NSString *roomId, NSNumber *openId); diff --git a/msext/Class/XiaoliaoSDK_iOS/xianliaoApi.a b/msext/Class/XiaoliaoSDK_iOS/xianliaoApi.a new file mode 100755 index 0000000..eb771da Binary files /dev/null and b/msext/Class/XiaoliaoSDK_iOS/xianliaoApi.a differ diff --git a/msext/Class/http/CocoaAsyncSocket/About.txt b/msext/Class/http/CocoaAsyncSocket/About.txt new file mode 100755 index 0000000..63547dd --- /dev/null +++ b/msext/Class/http/CocoaAsyncSocket/About.txt @@ -0,0 +1,4 @@ +The CocoaAsyncSocket project is under Public Domain license. +http://code.google.com/p/cocoaasyncsocket/ + +The AsyncSocket project has been around since 2001 and is used in many applications and frameworks. \ No newline at end of file diff --git a/msext/Class/http/CocoaAsyncSocket/GCDAsyncSocket.h b/msext/Class/http/CocoaAsyncSocket/GCDAsyncSocket.h new file mode 100755 index 0000000..cf9927f --- /dev/null +++ b/msext/Class/http/CocoaAsyncSocket/GCDAsyncSocket.h @@ -0,0 +1,1074 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; + +#if TARGET_OS_IPHONE + + // Compiling for iOS + + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000 // iOS 5.0 supported + + #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 // iOS 5.0 supported and required + + #define IS_SECURE_TRANSPORT_AVAILABLE YES + #define SECURE_TRANSPORT_MAYBE_AVAILABLE 1 + #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 0 + + #else // iOS 5.0 supported but not required + + #ifndef NSFoundationVersionNumber_iPhoneOS_5_0 + #define NSFoundationVersionNumber_iPhoneOS_5_0 881.00 + #endif + + #define IS_SECURE_TRANSPORT_AVAILABLE (NSFoundationVersionNumber >= NSFoundationVersionNumber_iPhoneOS_5_0) + #define SECURE_TRANSPORT_MAYBE_AVAILABLE 1 + #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 1 + + #endif + + #else // iOS 5.0 not supported + + #define IS_SECURE_TRANSPORT_AVAILABLE NO + #define SECURE_TRANSPORT_MAYBE_AVAILABLE 0 + #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 1 + + #endif + +#else + + // Compiling for Mac OS X + + #define IS_SECURE_TRANSPORT_AVAILABLE YES + #define SECURE_TRANSPORT_MAYBE_AVAILABLE 1 + #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 0 + +#endif + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +#if SECURE_TRANSPORT_MAYBE_AVAILABLE +extern NSString *const GCDAsyncSocketSSLCipherSuites; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +#else +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif +#endif + +enum GCDAsyncSocketError +{ + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; +typedef enum GCDAsyncSocketError GCDAsyncSocketError; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (id)init; +- (id)initWithSocketQueue:(dispatch_queue_t)sq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; + +#pragma mark Configuration + +- (id)delegate; +- (void)setDelegate:(id)delegate; +- (void)synchronouslySetDelegate:(id)delegate; + +- (dispatch_queue_t)delegateQueue; +- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue; + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr; +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ +- (BOOL)isIPv4Enabled; +- (void)setIPv4Enabled:(BOOL)flag; + +- (BOOL)isIPv6Enabled; +- (void)setIPv6Enabled:(BOOL)flag; + +- (BOOL)isIPv4PreferredOverIPv6; +- (void)setPreferIPv4OverIPv6:(BOOL)flag; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +- (id)userData; +- (void)setUserData:(id)arbitraryUserData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAdd +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +- (BOOL)isDisconnected; +- (BOOL)isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +- (NSString *)connectedHost; +- (uint16_t)connectedPort; + +- (NSString *)localHost; +- (uint16_t)localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * See also the connectedHost, connectedPort, localHost and localPort methods. +**/ +- (NSData *)connectedAddress; +- (NSData *)localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +- (BOOL)isIPv4; +- (BOOL)isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +- (BOOL)isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * The possible keys and values for the TLS settings are well documented. + * Standard keys are: + * + * - kCFStreamSSLLevel + * - kCFStreamSSLAllowsExpiredCertificates + * - kCFStreamSSLAllowsExpiredRoots + * - kCFStreamSSLAllowsAnyRoot + * - kCFStreamSSLValidatesCertificateChain + * - kCFStreamSSLPeerName + * - kCFStreamSSLCertificates + * - kCFStreamSSLIsServer + * + * If SecureTransport is available on iOS: + * + * - GCDAsyncSocketSSLCipherSuites + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * + * If SecureTransport is available on Mac OS X: + * + * - GCDAsyncSocketSSLCipherSuites + * - GCDAsyncSocketSSLDiffieHellmanParameters; + * + * + * Please refer to Apple's documentation for associated values, as well as other possible keys. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * If you do not know the peer name of the remote host in advance (for example, you're not sure + * if it will be "domain.com" or "www.domain.com"), then you can use the default settings to validate the + * certificate, and then use the X509Certificate class to verify the issuer after the socket has been secured. + * The X509Certificate class is part of the CocoaAsyncSocket open source project. + **/ +- (void)startTLS:(NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +- (BOOL)autoDisconnectOnClosedReadStream; +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (CFReadStreamRef)readStream; +- (CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +#if SECURE_TRANSPORT_MAYBE_AVAILABLE + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (SSLContextRef)sslContext; + +#endif + +#pragma mark Utilities + +/** + * Extracting host and port information from raw address data. +**/ ++ (NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * this delegate method will be called before the disconnect method returns. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +@end diff --git a/msext/Class/http/CocoaAsyncSocket/GCDAsyncSocket.m b/msext/Class/http/CocoaAsyncSocket/GCDAsyncSocket.m new file mode 100755 index 0000000..1ecc94a --- /dev/null +++ b/msext/Class/http/CocoaAsyncSocket/GCDAsyncSocket.m @@ -0,0 +1,7430 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + +/** + * Does ARC support support GCD objects? + * It does if the minimum deployment target is iOS 6+ or Mac OS X 10.8+ +**/ +#if TARGET_OS_IPHONE + + // Compiling for iOS + + #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later + #define NEEDS_DISPATCH_RETAIN_RELEASE 0 + #else // iOS 5.X or earlier + #define NEEDS_DISPATCH_RETAIN_RELEASE 1 + #endif + +#else + + // Compiling for Mac OS X + + #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later + #define NEEDS_DISPATCH_RETAIN_RELEASE 0 + #else + #define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier + #endif + +#endif + + +#if 0 + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext 65535 + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +// Log levels : off, error, warn, info, verbose +static const int logLevel = LOG_LEVEL_VERBOSE; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +#if SECURE_TRANSPORT_MAYBE_AVAILABLE +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +#else +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 16, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 17, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 18, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE + static NSThread *cfstreamThread; // Used for CFStreams +#endif + +@interface GCDAsyncSocket () +{ + uint32_t flags; + uint16_t config; + +#if __has_feature(objc_arc_weak) + __weak id delegate; +#else + __unsafe_unretained id delegate; +#endif + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int connectIndex; + NSData * connectInterface4; + NSData * connectInterface6; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif +#if SECURE_TRANSPORT_MAYBE_AVAILABLE + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; +#endif + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; +} +// Accepting +- (BOOL)doAccept:(int)socketFD; + +// Connecting +- (void)startConnectTimeout:(NSTimeInterval)timeout; +- (void)endConnectTimeout; +- (void)doConnectTimeout; +- (void)lookup:(int)aConnectIndex host:(NSString *)host port:(uint16_t)port; +- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6; +- (void)lookup:(int)aConnectIndex didFail:(NSError *)error; +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr; +- (void)didConnect:(int)aConnectIndex; +- (void)didNotConnect:(int)aConnectIndex error:(NSError *)error; + +// Disconnect +- (void)closeWithError:(NSError *)error; +- (void)maybeClose; + +// Errors +- (NSError *)badConfigError:(NSString *)msg; +- (NSError *)badParamError:(NSString *)msg; +- (NSError *)gaiError:(int)gai_error; +- (NSError *)errnoError; +- (NSError *)errnoErrorWithReason:(NSString *)reason; +- (NSError *)connectTimeoutError; +- (NSError *)otherError:(NSString *)msg; + +// Diagnostics +- (NSString *)connectedHost4; +- (NSString *)connectedHost6; +- (uint16_t)connectedPort4; +- (uint16_t)connectedPort6; +- (NSString *)localHost4; +- (NSString *)localHost6; +- (uint16_t)localPort4; +- (uint16_t)localPort6; +- (NSString *)connectedHostFromSocket4:(int)socketFD; +- (NSString *)connectedHostFromSocket6:(int)socketFD; +- (uint16_t)connectedPortFromSocket4:(int)socketFD; +- (uint16_t)connectedPortFromSocket6:(int)socketFD; +- (NSString *)localHostFromSocket4:(int)socketFD; +- (NSString *)localHostFromSocket6:(int)socketFD; +- (uint16_t)localPortFromSocket4:(int)socketFD; +- (uint16_t)localPortFromSocket6:(int)socketFD; + +// Utilities +- (void)getInterfaceAddress4:(NSMutableData **)addr4Ptr + address6:(NSMutableData **)addr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port; +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD; +- (void)suspendReadSource; +- (void)resumeReadSource; +- (void)suspendWriteSource; +- (void)resumeWriteSource; + +// Reading +- (void)maybeDequeueRead; +- (void)flushSSLBuffers; +- (void)doReadData; +- (void)doReadEOF; +- (void)completeCurrentRead; +- (void)endCurrentRead; +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout; +- (void)doReadTimeout; +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension; + +// Writing +- (void)maybeDequeueWrite; +- (void)doWriteData; +- (void)completeCurrentWrite; +- (void)endCurrentWrite; +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout; +- (void)doWriteTimeout; +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension; + +// Security +- (void)maybeStartTLS; +#if SECURE_TRANSPORT_MAYBE_AVAILABLE +- (void)ssl_startTLS; +- (void)ssl_continueSSLHandshake; +#endif +#if TARGET_OS_IPHONE +- (void)cf_startTLS; +#endif + +// CFStream +#if TARGET_OS_IPHONE ++ (void)startCFStreamThreadIfNeeded; +- (BOOL)createReadAndWriteStream; +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite; +- (BOOL)addStreamsToRunLoop; +- (BOOL)openStreams; +- (void)removeStreamsFromRunLoop; +#endif + +// Class Methods ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. +**/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (id)initWithCapacity:(size_t)numBytes; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +- (id)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = preBufferSize - (writePointer - readPointer); + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = writePointer - readPointer; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - readPointer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = preBufferSize - (writePointer - readPointer); +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface GCDAsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. +**/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. +**/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + + result = MIN(defaultValue, (readLength - bytesDone)); + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. +**/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. +**/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + uint8_t seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. +**/ +@interface GCDAsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation GCDAsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface GCDAsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation GCDAsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket + +- (id)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if NEEDS_DISPATCH_RETAIN_RELEASE + if (dq) dispatch_retain(dq); + #endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + connectIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_retain(sq); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + + #if NEEDS_DISPATCH_RETAIN_RELEASE + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if NEEDS_DISPATCH_RETAIN_RELEASE + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if NEEDS_DISPATCH_RETAIN_RELEASE + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if NEEDS_DISPATCH_RETAIN_RELEASE + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setPreferIPv4OverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kPreferIPv6; + else + config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + socket4FD = createSocket(AF_INET, interface4); + + if (socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + socket6FD = createSocket(AF_INET6, interface6); + + if (socket6FD == SOCKET_NULL) + { + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + int socketFD = socket4FD; + dispatch_source_t acceptSource = accept4Source; + + dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([self doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(accept4Source, ^{ + + #if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(accept4Source); + } + + if (enableIPv6) + { + accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + int socketFD = socket6FD; + dispatch_source_t acceptSource = accept6Source; + + dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([self doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(accept6Source, ^{ + + #if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(accept6Source); + } + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + BOOL isIPv4; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + isIPv4 = YES; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socket6FD) + { + isIPv4 = NO; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[GCDAsyncSocket alloc] initWithDelegate:theDelegate + delegateQueue:delegateQueue + socketQueue:childSocketQueue]; + + if (isIPv4) + acceptedSocket->socket4FD = childSocketFD; + else + acceptedSocket->socket6FD = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) + #if NEEDS_DISPATCH_RETAIN_RELEASE + if (childSocketQueue) dispatch_release(childSocketQueue); + #endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * +**/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + int aConnectIndex = connectIndex; + NSString *hostCpy = [host copy]; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + + [self lookup:aConnectIndex host:hostCpy port:port]; + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aConnectIndex host:(NSString *)host port:(uint16_t)port +{ + LogTrace(); + + // This method is executed on a global concurrent queue. + // It posts the results back to the socket queue. + // The lookupIndex is used to ignore the results if the connect operation was cancelled or timed out. + + NSError *error = nil; + + NSData *address4 = nil; + NSData *address6 = nil; + + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr; + nativeAddr.sin_len = sizeof(struct sockaddr_in); + nativeAddr.sin_family = AF_INET; + nativeAddr.sin_port = htons(port); + nativeAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + address4 = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)]; + address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + for(res = res0; res; res = res->ai_next) + { + if ((address4 == nil) && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structure + address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if ((address6 == nil) && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structure + address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + + if ((address4 == nil) && (address6 == nil)) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self lookup:aConnectIndex didFail:error]; + }}); + } + else + { + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self lookup:aConnectIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } +} + +- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aConnectIndex != connectIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. +**/ +- (void)lookup:(int)aConnectIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aConnectIndex != connectIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + // Create the socket + + int socketFD; + NSData *address; + NSData *connectInterface; + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = socket(AF_INET6, SOCK_STREAM, 0); + + socketFD = socket6FD; + address = address6; + connectInterface = connectInterface6; + } + else + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = socket(AF_INET, SOCK_STREAM, 0); + + socketFD = socket4FD; + address = address4; + connectInterface = connectInterface4; + } + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + + return NO; + } + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aConnectIndex = connectIndex; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + if (result == 0) + { + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didConnect:aConnectIndex]; + }}); + } + else + { + NSError *error = [self errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didNotConnect:aConnectIndex error:error]; + }}); + } + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aConnectIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aConnectIndex != connectIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the connectIndex. + aConnectIndex = connectIndex; + #endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ + #if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + #endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ + #if TARGET_OS_IPHONE + + if (aConnectIndex != connectIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + #endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : socket6FD; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aConnectIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aConnectIndex != connectIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + + [self doConnectTimeout]; + }}); + + #if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment connectIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + connectIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + + #if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } + #endif + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + { + [sslPreBuffer reset]; + sslErrCode = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + + #if TARGET_OS_IPHONE + CFRelease(sslContext); + #else + SSLDisposeContext(sslContext); + #endif + + sslContext = NULL; + } + } + #endif + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted); + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + + if (shouldCallDelegate) + { + if (delegateQueue && [delegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:self withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + +- (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. +**/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { + + LogVerbose(@"readEventBlock"); + + socketFDBytesAvailable = dispatch_source_get_data(readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", socketFDBytesAvailable); + + if (socketFDBytesAvailable > 0) + [self doReadData]; + else + [self doReadEOF]; + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { + + LogVerbose(@"writeEventBlock"); + + flags |= kSocketCanAcceptBytes; + [self doWriteData]; + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + #if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + + LogVerbose(@"readCancelBlock"); + + #if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ + + LogVerbose(@"writeCancelBlock"); + + #if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ + #if TARGET_OS_IPHONE + { + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // Due to the fact that Apple doesn't give us the full power of SecureTransport on iOS, + // we are relegated to using the slower, less powerful, and RunLoop based CFStream API. :( Boo! + // + // Thus we're not able to use the GCD read/write sources in this particular scenario. + + return YES; + } + } + #endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + #if TARGET_OS_IPHONE + { + return ![self usingCFStreamForTLS]; + } + #endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = currentRead->bytesDone; + NSUInteger total = currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + +#if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + +#endif +#if SECURE_TRANSPORT_MAYBE_AVAILABLE + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } + +#endif +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Relegated to using CFStream... :( Boo! Give us a full SecureTransport stack Apple! + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + + #endif + } + else + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + + #endif + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS]) + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + + #endif + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occured + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && !waiting && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + BOOL readIntoPreBuffer = NO; + NSUInteger bytesToRead; + + if ([self usingCFStreamForTLS]) + { + // Since Apple hasn't made the full power of SecureTransport available on iOS, + // we are relegated to using the slower, less powerful, RunLoop based CFStream API. + // + // This API doesn't tell us how much data is available on the socket to be read. + // If we had that information we could optimize our memory allocations, and sys calls. + // + // But alas... + // So we do it old school, and just read as much data from the socket as we can. + + NSUInteger defaultReadLength = (1024 * 32); + + bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + } + + if (bytesToRead > SIZE_MAX) // NSUInteger may be bigger than size_t (read param 3) + { + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + uint8_t *buffer; + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; + } + + // Read data into buffer + + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + + #endif + } + else + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + + #endif + } + } + else + { + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errnoErrorWithReason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + bytesToRead = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToRead); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToRead]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToRead; + totalBytesReadForCurrentRead += bytesToRead; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && !waiting && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + __strong id theDelegate = delegate; + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + error = [self sslError:errSSLClosedAbort]; + #endif + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + if (delegateQueue && [delegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + #endif + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + __strong id theDelegate = delegate; + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { + + [self doReadTimeout]; + }}); + + #if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + __strong id theDelegate = delegate; + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = currentWrite->bytesDone; + NSUInteger total = [currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS]) + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + + #endif + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + + #endif + } + else + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + size_t sslBytesToWrite = MIN(bytesRemaining, 32768); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + + #endif + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errnoErrorWithReason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + [self maybeDequeueWrite]; + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting & !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + __strong id theDelegate = delegate; + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + __strong id theDelegate = delegate; + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { + + [self doWriteTimeout]; + }}); + + #if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + if (delegateQueue && [delegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + __strong id theDelegate = delegate; + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (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]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL canUseSecureTransport = YES; + + #if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + NSNumber *value; + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot]; + if (value && [value boolValue] == YES) + canUseSecureTransport = NO; + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots]; + if (value && [value boolValue] == YES) + canUseSecureTransport = NO; + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain]; + if (value && [value boolValue] == NO) + canUseSecureTransport = NO; + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates]; + if (value && [value boolValue] == YES) + canUseSecureTransport = NO; + } + #endif + + if (IS_SECURE_TRANSPORT_AVAILABLE && canUseSecureTransport) + { + #if SECURE_TRANSPORT_MAYBE_AVAILABLE + [self ssl_startTLS]; + #endif + } + else + { + #if TARGET_OS_IPHONE + [self cf_startTLS]; + #endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if SECURE_TRANSPORT_MAYBE_AVAILABLE + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + 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]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + BOOL isServer = [[tlsSettings objectForKey:(NSString *)kCFStreamSSLIsServer] boolValue]; + + #if TARGET_OS_IPHONE + { + 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 + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } + #endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLAllowsAnyRoot + // 3. kCFStreamSSLAllowsExpiredRoots + // 4. kCFStreamSSLValidatesCertificateChain + // 5. kCFStreamSSLAllowsExpiredCertificates + // 6. kCFStreamSSLCertificates + // 7. kCFStreamSSLLevel (GCDAsyncSocketSSLProtocolVersionMin / GCDAsyncSocketSSLProtocolVersionMax) + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + + id value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + + // 2. kCFStreamSSLAllowsAnyRoot + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot]; + if (value) + { + #if TARGET_OS_IPHONE + NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsAnyRoot"); + #else + + BOOL allowsAnyRoot = [value boolValue]; + + status = SSLSetAllowsAnyRoot(sslContext, allowsAnyRoot); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetAllowsAnyRoot"]]; + return; + } + + #endif + } + + // 3. kCFStreamSSLAllowsExpiredRoots + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots]; + if (value) + { + #if TARGET_OS_IPHONE + NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsExpiredRoots"); + #else + + BOOL allowsExpiredRoots = [value boolValue]; + + status = SSLSetAllowsExpiredRoots(sslContext, allowsExpiredRoots); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredRoots"]]; + return; + } + + #endif + } + + // 4. kCFStreamSSLValidatesCertificateChain + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain]; + if (value) + { + #if TARGET_OS_IPHONE + NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLValidatesCertificateChain"); + #else + + BOOL validatesCertChain = [value boolValue]; + + status = SSLSetEnableCertVerify(sslContext, validatesCertChain); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + + #endif + } + + // 5. kCFStreamSSLAllowsExpiredCertificates + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates]; + if (value) + { + #if TARGET_OS_IPHONE + NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsExpiredCertificates"); + #else + + BOOL allowsExpiredCerts = [value boolValue]; + + status = SSLSetAllowsExpiredCerts(sslContext, allowsExpiredCerts); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredCerts"]]; + return; + } + + #endif + } + + // 6. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLCertificates]; + if (value) + { + CFArrayRef certs = (__bridge CFArrayRef)value; + + status = SSLSetCertificate(sslContext, certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + + // 7. kCFStreamSSLLevel + + #if TARGET_OS_IPHONE + { + NSString *sslLevel = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel]; + + NSString *sslMinLevel = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + NSString *sslMaxLevel = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + + if (sslLevel) + { + if (sslMinLevel || sslMaxLevel) + { + LogWarn(@"kCFStreamSSLLevel security option ignored. Overriden by " + @"GCDAsyncSocketSSLProtocolVersionMin and/or GCDAsyncSocketSSLProtocolVersionMax"); + } + else + { + if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv3]) + { + sslMinLevel = sslMaxLevel = @"kSSLProtocol3"; + } + else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelTLSv1]) + { + sslMinLevel = sslMaxLevel = @"kTLSProtocol1"; + } + else + { + LogWarn(@"Unable to match kCFStreamSSLLevel security option to valid SSL protocol min/max"); + } + } + } + + if (sslMinLevel || sslMaxLevel) + { + OSStatus status1 = noErr; + OSStatus status2 = noErr; + + SSLProtocol (^sslProtocolForString)(NSString*) = ^SSLProtocol (NSString *protocolStr) { + + if ([protocolStr isEqualToString:@"kSSLProtocol3"]) return kSSLProtocol3; + if ([protocolStr isEqualToString:@"kTLSProtocol1"]) return kTLSProtocol1; + if ([protocolStr isEqualToString:@"kTLSProtocol11"]) return kTLSProtocol11; + if ([protocolStr isEqualToString:@"kTLSProtocol12"]) return kTLSProtocol12; + + return kSSLProtocolUnknown; + }; + + SSLProtocol minProtocol = sslProtocolForString(sslMinLevel); + SSLProtocol maxProtocol = sslProtocolForString(sslMaxLevel); + + if (minProtocol != kSSLProtocolUnknown) + { + status1 = SSLSetProtocolVersionMin(sslContext, minProtocol); + } + if (maxProtocol != kSSLProtocolUnknown) + { + status2 = SSLSetProtocolVersionMax(sslContext, maxProtocol); + } + + if (status1 != noErr || status2 != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMinMax"]]; + return; + } + } + } + #else + { + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel]; + if (value) + { + NSString *sslLevel = (NSString *)value; + + OSStatus status1 = noErr; + OSStatus status2 = noErr; + OSStatus status3 = noErr; + + if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv2]) + { + // kCFStreamSocketSecurityLevelSSLv2: + // + // Specifies that SSL version 2 be set as the security protocol. + + status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO); + status2 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES); + } + else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv3]) + { + // kCFStreamSocketSecurityLevelSSLv3: + // + // Specifies that SSL version 3 be set as the security protocol. + // If SSL version 3 is not available, specifies that SSL version 2 be set as the security protocol. + + status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO); + status2 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES); + status3 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol3, YES); + } + else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelTLSv1]) + { + // kCFStreamSocketSecurityLevelTLSv1: + // + // Specifies that TLS version 1 be set as the security protocol. + + status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO); + status2 = SSLSetProtocolVersionEnabled(sslContext, kTLSProtocol1, YES); + } + else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL]) + { + // kCFStreamSocketSecurityLevelNegotiatedSSL: + // + // Specifies that the highest level security protocol that can be negotiated be used. + + status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, YES); + } + + if (status1 != noErr || status2 != noErr || status3 != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionEnabled"]]; + return; + } + } + } + #endif + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if (value) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; + SSLCipherSuite ciphers[numberCiphers]; + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = [cipherObject shortValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + + #if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if (value) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + #endif + + // 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. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)]) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + 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]]; + } +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)]) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + 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; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + 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..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)startCFStreamThreadIfNeeded +{ + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread) + object:nil]; + [cfstreamThread start]; + }); +} + ++ (void)cfstreamThread { @autoreleasepool +{ + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(doNothingAtAll:) + userInfo:nil + repeats:YES]; + + [[NSRunLoop currentRunLoop] run]; + + LogInfo(@"CFStreamThread: Stopped"); +}} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. +**/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. +**/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kAllowHalfDuplexConnection; + else + config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? +**/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? +**/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occured creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? +**/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +#if SECURE_TRANSPORT_MAYBE_AVAILABLE + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDASLLogger.h b/msext/Class/http/CocoaLumberjack/DDASLLogger.h new file mode 100755 index 0000000..0f2e963 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDASLLogger.h @@ -0,0 +1,41 @@ +#import +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs it's output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. +**/ + +@interface DDASLLogger : DDAbstractLogger +{ + aslclient client; +} + ++ (DDASLLogger *)sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDASLLogger.m b/msext/Class/http/CocoaLumberjack/DDASLLogger.m new file mode 100755 index 0000000..0c35f2f --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDASLLogger.m @@ -0,0 +1,99 @@ +#import "DDASLLogger.h" + +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation DDASLLogger + +static DDASLLogger *sharedInstance; + +/** + * The runtime sends initialize to each class in a program exactly one time just before the class, + * or any class that inherits from it, is sent its first message from within the program. (Thus the + * method may never be invoked if the class is not used.) The runtime sends the initialize message to + * classes in a thread-safe manner. Superclasses receive this message before their subclasses. + * + * This method may also be called directly (assumably by accident), hence the safety mechanism. +**/ ++ (void)initialize +{ + static BOOL initialized = NO; + if (!initialized) + { + initialized = YES; + + sharedInstance = [[DDASLLogger alloc] init]; + } +} + ++ (DDASLLogger *)sharedInstance +{ + return sharedInstance; +} + +- (id)init +{ + if (sharedInstance != nil) + { + return nil; + } + + if ((self = [super init])) + { + // A default asl client is provided for the main thread, + // but background threads need to create their own client. + + client = asl_open(NULL, "com.apple.console", 0); + } + return self; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + NSString *logMsg = logMessage->logMsg; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + } + + if (logMsg) + { + const char *msg = [logMsg UTF8String]; + + int aslLogLevel; + switch (logMessage->logFlag) + { + // Note: By default ASL will filter anything above level 5 (Notice). + // So our mappings shouldn't go above that level. + + case LOG_FLAG_ERROR : aslLogLevel = ASL_LEVEL_CRIT; break; + case LOG_FLAG_WARN : aslLogLevel = ASL_LEVEL_ERR; break; + case LOG_FLAG_INFO : aslLogLevel = ASL_LEVEL_WARNING; break; + default : aslLogLevel = ASL_LEVEL_NOTICE; break; + } + + asl_log(client, NULL, aslLogLevel, "%s", msg); + } +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.aslLogger"; +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDAbstractDatabaseLogger.h b/msext/Class/http/CocoaLumberjack/DDAbstractDatabaseLogger.h new file mode 100755 index 0000000..436234e --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDAbstractDatabaseLogger.h @@ -0,0 +1,102 @@ +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides an abstract implementation of a database logger. + * + * That is, it provides the base implementation for a database logger to build atop of. + * All that is needed for a concrete database logger is to extend this class + * and override the methods in the implementation file that are prefixed with "db_". +**/ + +@interface DDAbstractDatabaseLogger : DDAbstractLogger { +@protected + NSUInteger saveThreshold; + NSTimeInterval saveInterval; + NSTimeInterval maxAge; + NSTimeInterval deleteInterval; + BOOL deleteOnEverySave; + + BOOL saveTimerSuspended; + NSUInteger unsavedCount; + dispatch_time_t unsavedTime; + dispatch_source_t saveTimer; + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; +} + +/** + * Specifies how often to save the data to disk. + * Since saving is an expensive operation (disk io) it is not done after every log statement. + * These properties allow you to configure how/when the logger saves to disk. + * + * A save is done when either (whichever happens first): + * + * - The number of unsaved log entries reaches saveThreshold + * - The amount of time since the oldest unsaved log entry was created reaches saveInterval + * + * You can optionally disable the saveThreshold by setting it to zero. + * If you disable the saveThreshold you are entirely dependent on the saveInterval. + * + * You can optionally disable the saveInterval by setting it to zero (or a negative value). + * If you disable the saveInterval you are entirely dependent on the saveThreshold. + * + * It's not wise to disable both saveThreshold and saveInterval. + * + * The default saveThreshold is 500. + * The default saveInterval is 60 seconds. +**/ +@property (assign, readwrite) NSUInteger saveThreshold; +@property (assign, readwrite) NSTimeInterval saveInterval; + +/** + * It is likely you don't want the log entries to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxAge property provides a way to specify how old a log statement can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old log entries. + * Since deleting is an expensive operation (disk io) is is done on a fixed interval. + * + * An alternative to the deleteInterval is the deleteOnEverySave option. + * This specifies that old log entries should be deleted during every save operation. + * + * You can optionally disable the maxAge by setting it to zero (or a negative value). + * If you disable the maxAge then old log statements are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. + * + * It's not wise to enable both deleteInterval and deleteOnEverySave. + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. + * The default deleteOnEverySave is NO. +**/ +@property (assign, readwrite) NSTimeInterval maxAge; +@property (assign, readwrite) NSTimeInterval deleteInterval; +@property (assign, readwrite) BOOL deleteOnEverySave; + +/** + * Forces a save of any pending log entries (flushes log entries to disk). +**/ +- (void)savePendingLogEntries; + +/** + * Removes any log entries that are older than maxAge. +**/ +- (void)deleteOldLogEntries; + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDAbstractDatabaseLogger.m b/msext/Class/http/CocoaLumberjack/DDAbstractDatabaseLogger.m new file mode 100755 index 0000000..c7366a6 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDAbstractDatabaseLogger.m @@ -0,0 +1,727 @@ +#import "DDAbstractDatabaseLogger.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface DDAbstractDatabaseLogger () +- (void)destroySaveTimer; +- (void)destroyDeleteTimer; +@end + +#pragma mark - + +@implementation DDAbstractDatabaseLogger + +- (id)init +{ + if ((self = [super init])) + { + saveThreshold = 500; + saveInterval = 60; // 60 seconds + maxAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 minutes + } + return self; +} + +- (void)dealloc +{ + [self destroySaveTimer]; + [self destroyDeleteTimer]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Override Me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)db_log:(DDLogMessage *)logMessage +{ + // Override me and add your implementation. + // + // Return YES if an item was added to the buffer. + // Return NO if the logMessage was ignored. + + return NO; +} + +- (void)db_save +{ + // Override me and add your implementation. +} + +- (void)db_delete +{ + // Override me and add your implementation. +} + +- (void)db_saveAndDelete +{ + // Override me and add your implementation. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performSaveAndSuspendSaveTimer +{ + if (unsavedCount > 0) + { + if (deleteOnEverySave) + [self db_saveAndDelete]; + else + [self db_save]; + } + + unsavedCount = 0; + unsavedTime = 0; + + if (saveTimer && !saveTimerSuspended) + { + dispatch_suspend(saveTimer); + saveTimerSuspended = YES; + } +} + +- (void)performDelete +{ + if (maxAge > 0.0) + { + [self db_delete]; + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)destroySaveTimer +{ + if (saveTimer) + { + dispatch_source_cancel(saveTimer); + if (saveTimerSuspended) + { + // Must resume a timer before releasing it (or it will crash) + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + #if !OS_OBJECT_USE_OBJC + dispatch_release(saveTimer); + #endif + saveTimer = NULL; + } +} + +- (void)updateAndResumeSaveTimer +{ + if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0)) + { + uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC); + dispatch_time_t startTime = dispatch_time(unsavedTime, interval); + + dispatch_source_set_timer(saveTimer, startTime, interval, 1.0); + + if (saveTimerSuspended) + { + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + } +} + +- (void)createSuspendedSaveTimer +{ + if ((saveTimer == NULL) && (saveInterval > 0.0)) + { + saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + + }}); + + saveTimerSuspended = YES; + } +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC); + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + if (deleteTimer != NULL) { + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + dispatch_resume(deleteTimer); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)saveThreshold +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSUInteger result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveThreshold; + }); + }); + + return result; +} + +- (void)setSaveThreshold:(NSUInteger)threshold +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (saveThreshold != threshold) + { + saveThreshold = threshold; + + // Since the saveThreshold has changed, + // we check to see if the current unsavedCount has surpassed the new threshold. + // + // If it has, we immediately save the log. + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)saveInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveInterval; + }); + }); + + return result; +} + +- (void)setSaveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* saveInterval != interval */ islessgreater(saveInterval, interval)) + { + saveInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the saveInterval was previously enabled and it just got disabled, + // then we need to stop the saveTimer. (And we might as well release it.) + // + // 2. If the saveInterval was previously disabled and it just got enabled, + // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) + // + // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate save.) + + if (saveInterval > 0.0) + { + if (saveTimer == NULL) + { + // Handles #2 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self createSuspendedSaveTimer]; + [self updateAndResumeSaveTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateAndResumeSaveTimer]; + } + } + else if (saveTimer) + { + // Handles #1 + + [self destroySaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)maxAge +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = maxAge; + }); + }); + + return result; +} + +- (void)setMaxAge:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* maxAge != interval */ islessgreater(maxAge, interval)) + { + NSTimeInterval oldMaxAge = maxAge; + NSTimeInterval newMaxAge = interval; + + maxAge = interval; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxAge > 0.0) + { + if (newMaxAge <= 0.0) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + else if (oldMaxAge > newMaxAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + } + else if (newMaxAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)deleteInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteInterval; + }); + }); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval)) + { + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (BOOL)deleteOnEverySave +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteOnEverySave; + }); + }); + + return result; +} + +- (void)setDeleteOnEverySave:(BOOL)flag +{ + dispatch_block_t block = ^{ + + deleteOnEverySave = flag; + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)savePendingLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +- (void)deleteOldLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performDelete]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddLogger +{ + // If you override me be sure to invoke [super didAddLogger]; + + [self createSuspendedSaveTimer]; + + [self createAndStartDeleteTimer]; +} + +- (void)willRemoveLogger +{ + // If you override me be sure to invoke [super willRemoveLogger]; + + [self performSaveAndSuspendSaveTimer]; + + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + if ([self db_log:logMessage]) + { + BOOL firstUnsavedEntry = (++unsavedCount == 1); + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + else if (firstUnsavedEntry) + { + unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); + [self updateAndResumeSaveTimer]; + } + } +} + +- (void)flush +{ + // This method is invoked by DDLog's flushLog method. + // + // It is called automatically when the application quits, + // or if the developer invokes DDLog's flushLog method prior to crashing or something. + + [self performSaveAndSuspendSaveTimer]; +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDFileLogger.h b/msext/Class/http/CocoaLumberjack/DDFileLogger.h new file mode 100755 index 0000000..5af6376 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDFileLogger.h @@ -0,0 +1,334 @@ +#import +#import "DDLog.h" + +@class DDLogFileInfo; + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger to write log statements to a file. +**/ + + +// Default configuration and safety/sanity values. +// +// maximumFileSize -> DEFAULT_LOG_MAX_FILE_SIZE +// rollingFrequency -> DEFAULT_LOG_ROLLING_FREQUENCY +// maximumNumberOfLogFiles -> DEFAULT_LOG_MAX_NUM_LOG_FILES +// +// You should carefully consider the proper configuration values for your application. + +#define DEFAULT_LOG_MAX_FILE_SIZE (1024 * 1024) // 1 MB +#define DEFAULT_LOG_ROLLING_FREQUENCY (60 * 60 * 24) // 24 Hours +#define DEFAULT_LOG_MAX_NUM_LOG_FILES (5) // 5 Files + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// The LogFileManager protocol is designed to allow you to control all aspects of your log files. +// +// The primary purpose of this is to allow you to do something with the log files after they have been rolled. +// Perhaps you want to compress them to save disk space. +// Perhaps you want to upload them to an FTP server. +// Perhaps you want to run some analytics on the file. +// +// A default LogFileManager is, of course, provided. +// The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. +// +// This protocol provides various methods to fetch the list of log files. +// +// There are two variants: sorted and unsorted. +// If sorting is not necessary, the unsorted variant is obviously faster. +// The sorted variant will return an array sorted by when the log files were created, +// with the most recently created log file at index 0, and the oldest log file at the end of the array. +// +// You can fetch only the log file paths (full path including name), log file names (name only), +// or an array of DDLogFileInfo objects. +// The DDLogFileInfo class is documented below, and provides a handy wrapper that +// gives you easy access to various file attributes such as the creation date or the file size. + +@protocol DDLogFileManager +@required + +// Public properties + +/** + * The maximum number of archived log files to keep on disk. + * For example, if this property is set to 3, + * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. + * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. + * + * You may optionally disable deleting old/rolled/archived log files by setting this property to zero. +**/ +@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles; + +// Public methods + +- (NSString *)logsDirectory; + +- (NSArray *)unsortedLogFilePaths; +- (NSArray *)unsortedLogFileNames; +- (NSArray *)unsortedLogFileInfos; + +- (NSArray *)sortedLogFilePaths; +- (NSArray *)sortedLogFileNames; +- (NSArray *)sortedLogFileInfos; + +// Private methods (only to be used by DDFileLogger) + +- (NSString *)createNewLogFile; + +@optional + +// Notifications from DDFileLogger + +- (void)didArchiveLogFile:(NSString *)logFilePath; +- (void)didRollAndArchiveLogFile:(NSString *)logFilePath; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default log file manager. + * + * All log files are placed inside the logsDirectory. + * If a specific logsDirectory isn't specified, the default directory is used. + * On Mac, this is in ~/Library/Logs/. + * On iPhone, this is in ~/Library/Caches/Logs. + * + * Log files are named "log-.txt", + * where uuid is a 6 character hexadecimal consisting of the set [0123456789ABCDEF]. + * + * Archived log files are automatically deleted according to the maximumNumberOfLogFiles property. +**/ +@interface DDLogFileManagerDefault : NSObject +{ + NSUInteger maximumNumberOfLogFiles; + NSString *_logsDirectory; +} + +- (id)init; +- (id)initWithLogsDirectory:(NSString *)logsDirectory; + +/* Inherited from DDLogFileManager protocol: + +@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles; + +- (NSString *)logsDirectory; + +- (NSArray *)unsortedLogFilePaths; +- (NSArray *)unsortedLogFileNames; +- (NSArray *)unsortedLogFileInfos; + +- (NSArray *)sortedLogFilePaths; +- (NSArray *)sortedLogFileNames; +- (NSArray *)sortedLogFileInfos; + +*/ + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Most users will want file log messages to be prepended with the date and time. + * Rather than forcing the majority of users to write their own formatter, + * we will supply a logical default formatter. + * Users can easily replace this formatter with their own by invoking the setLogFormatter method. + * It can also be removed by calling setLogFormatter, and passing a nil parameter. + * + * In addition to the convenience of having a logical default formatter, + * it will also provide a template that makes it easy for developers to copy and change. +**/ +@interface DDLogFileFormatterDefault : NSObject +{ + NSDateFormatter *dateFormatter; +} + +- (id)init; +- (id)initWithDateFormatter:(NSDateFormatter *)dateFormatter; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface DDFileLogger : DDAbstractLogger +{ + __strong id logFileManager; + + DDLogFileInfo *currentLogFileInfo; + NSFileHandle *currentLogFileHandle; + + dispatch_source_t rollingTimer; + + unsigned long long maximumFileSize; + NSTimeInterval rollingFrequency; +} + +- (id)init; +- (id)initWithLogFileManager:(id )logFileManager; + +/** + * Log File Rolling: + * + * maximumFileSize: + * The approximate maximum size to allow log files to grow. + * If a log file is larger than this value after a log statement is appended, + * then the log file is rolled. + * + * rollingFrequency + * How often to roll the log file. + * The frequency is given as an NSTimeInterval, which is a double that specifies the interval in seconds. + * Once the log file gets to be this old, it is rolled. + * + * Both the maximumFileSize and the rollingFrequency are used to manage rolling. + * Whichever occurs first will cause the log file to be rolled. + * + * For example: + * The rollingFrequency is 24 hours, + * but the log file surpasses the maximumFileSize after only 20 hours. + * The log file will be rolled at that 20 hour mark. + * A new log file will be created, and the 24 hour timer will be restarted. + * + * You may optionally disable rolling due to filesize by setting maximumFileSize to zero. + * If you do so, rolling is based solely on rollingFrequency. + * + * You may optionally disable rolling due to time by setting rollingFrequency to zero (or any non-positive number). + * If you do so, rolling is based solely on maximumFileSize. + * + * If you disable both maximumFileSize and rollingFrequency, then the log file won't ever be rolled. + * This is strongly discouraged. +**/ +@property (readwrite, assign) unsigned long long maximumFileSize; +@property (readwrite, assign) NSTimeInterval rollingFrequency; + +/** + * The DDLogFileManager instance can be used to retrieve the list of log files, + * and configure the maximum number of archived log files to keep. + * + * @see DDLogFileManager.maximumNumberOfLogFiles +**/ +@property (strong, nonatomic, readonly) id logFileManager; + + +// You can optionally force the current log file to be rolled with this method. + +- (void)rollLogFile; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * DDLogFileInfo is a simple class that provides access to various file attributes. + * It provides good performance as it only fetches the information if requested, + * and it caches the information to prevent duplicate fetches. + * + * It was designed to provide quick snapshots of the current state of log files, + * and to help sort log files in an array. + * + * This class does not monitor the files, or update it's cached attribute values if the file changes on disk. + * This is not what the class was designed for. + * + * If you absolutely must get updated values, + * you can invoke the reset method which will clear the cache. +**/ +@interface DDLogFileInfo : NSObject +{ + __strong NSString *filePath; + __strong NSString *fileName; + + __strong NSDictionary *fileAttributes; + + __strong NSDate *creationDate; + __strong NSDate *modificationDate; + + unsigned long long fileSize; +} + +@property (strong, nonatomic, readonly) NSString *filePath; +@property (strong, nonatomic, readonly) NSString *fileName; + +@property (strong, nonatomic, readonly) NSDictionary *fileAttributes; + +@property (strong, nonatomic, readonly) NSDate *creationDate; +@property (strong, nonatomic, readonly) NSDate *modificationDate; + +@property (nonatomic, readonly) unsigned long long fileSize; + +@property (nonatomic, readonly) NSTimeInterval age; + +@property (nonatomic, readwrite) BOOL isArchived; + ++ (id)logFileWithPath:(NSString *)filePath; + +- (id)initWithFilePath:(NSString *)filePath; + +- (void)reset; +- (void)renameFile:(NSString *)newFileName; + +#if TARGET_IPHONE_SIMULATOR + +// So here's the situation. +// Extended attributes are perfect for what we're trying to do here (marking files as archived). +// This is exactly what extended attributes were designed for. +// +// But Apple screws us over on the simulator. +// Everytime you build-and-go, they copy the application into a new folder on the hard drive, +// and as part of the process they strip extended attributes from our log files. +// Normally, a copy of a file preserves extended attributes. +// So obviously Apple has gone to great lengths to piss us off. +// +// Thus we use a slightly different tactic for marking log files as archived in the simulator. +// That way it "just works" and there's no confusion when testing. +// +// The difference in method names is indicative of the difference in functionality. +// On the simulator we add an attribute by appending a filename extension. +// +// For example: +// log-ABC123.txt -> log-ABC123.archived.txt + +- (BOOL)hasExtensionAttributeWithName:(NSString *)attrName; + +- (void)addExtensionAttributeWithName:(NSString *)attrName; +- (void)removeExtensionAttributeWithName:(NSString *)attrName; + +#else + +// Normal use of extended attributes used everywhere else, +// such as on Macs and on iPhone devices. + +- (BOOL)hasExtendedAttributeWithName:(NSString *)attrName; + +- (void)addExtendedAttributeWithName:(NSString *)attrName; +- (void)removeExtendedAttributeWithName:(NSString *)attrName; + +#endif + +- (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another; +- (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another; + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDFileLogger.m b/msext/Class/http/CocoaLumberjack/DDFileLogger.m new file mode 100755 index 0000000..afe7cc1 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDFileLogger.m @@ -0,0 +1,1353 @@ +#import "DDFileLogger.h" + +#import +#import +#import +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// We probably shouldn't be using DDLog() statements within the DDLog implementation. +// But we still want to leave our log statements for any future debugging, +// and to allow other developers to trace the implementation (which is a great learning tool). +// +// So we use primitive logging macros around NSLog. +// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. + +#define LOG_LEVEL 2 + +#define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) + +@interface DDLogFileManagerDefault (PrivateAPI) + +- (void)deleteOldLogFiles; +- (NSString *)defaultLogsDirectory; + +@end + +@interface DDFileLogger (PrivateAPI) + +- (void)rollLogFileNow; +- (void)maybeRollLogFileDueToAge; +- (void)maybeRollLogFileDueToSize; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLogFileManagerDefault + +@synthesize maximumNumberOfLogFiles; + +- (id)init +{ + return [self initWithLogsDirectory:nil]; +} + +- (id)initWithLogsDirectory:(NSString *)aLogsDirectory +{ + if ((self = [super init])) + { + maximumNumberOfLogFiles = DEFAULT_LOG_MAX_NUM_LOG_FILES; + + if (aLogsDirectory) + _logsDirectory = [aLogsDirectory copy]; + else + _logsDirectory = [[self defaultLogsDirectory] copy]; + + NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; + + [self addObserver:self forKeyPath:@"maximumNumberOfLogFiles" options:kvoOptions context:nil]; + + NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]); + NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]); + } + return self; +} + +- (void)dealloc +{ + [self removeObserver:self forKeyPath:@"maximumNumberOfLogFiles"]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + NSNumber *old = [change objectForKey:NSKeyValueChangeOldKey]; + NSNumber *new = [change objectForKey:NSKeyValueChangeNewKey]; + + if ([old isEqual:new]) + { + // No change in value - don't bother with any processing. + return; + } + + if ([keyPath isEqualToString:@"maximumNumberOfLogFiles"]) + { + NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: maximumNumberOfLogFiles"); + + dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool { + + [self deleteOldLogFiles]; + }}); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark File Deleting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Deletes archived log files that exceed the maximumNumberOfLogFiles configuration value. +**/ +- (void)deleteOldLogFiles +{ + NSLogVerbose(@"DDLogFileManagerDefault: deleteOldLogFiles"); + + NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles; + if (maxNumLogFiles == 0) + { + // Unlimited - don't delete any log files + return; + } + + NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; + + // Do we consider the first file? + // We are only supposed to be deleting archived files. + // In most cases, the first file is likely the log file that is currently being written to. + // So in most cases, we do not want to consider this file for deletion. + + NSUInteger count = [sortedLogFileInfos count]; + BOOL excludeFirstFile = NO; + + if (count > 0) + { + DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:0]; + + if (!logFileInfo.isArchived) + { + excludeFirstFile = YES; + } + } + + NSArray *sortedArchivedLogFileInfos; + if (excludeFirstFile) + { + count--; + sortedArchivedLogFileInfos = [sortedLogFileInfos subarrayWithRange:NSMakeRange(1, count)]; + } + else + { + sortedArchivedLogFileInfos = sortedLogFileInfos; + } + + NSUInteger i; + for (i = maxNumLogFiles; i < count; i++) + { + DDLogFileInfo *logFileInfo = [sortedArchivedLogFileInfos objectAtIndex:i]; + + NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName); + + [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Log Files +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the path to the default logs directory. + * If the logs directory doesn't exist, this method automatically creates it. +**/ +- (NSString *)defaultLogsDirectory +{ +#if TARGET_OS_IPHONE + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *baseDir = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; + NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"]; + +#else + NSString *appName = [[NSProcessInfo processInfo] processName]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory(); + NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"Logs"] stringByAppendingPathComponent:appName]; + +#endif + + return logsDirectory; +} + +- (NSString *)logsDirectory +{ + // We could do this check once, during initalization, and not bother again. + // But this way the code continues to work if the directory gets deleted while the code is running. + + if (![[NSFileManager defaultManager] fileExistsAtPath:_logsDirectory]) + { + NSError *err = nil; + if (![[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory + withIntermediateDirectories:YES attributes:nil error:&err]) + { + NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", err); + } + } + + return _logsDirectory; +} + +- (BOOL)isLogFile:(NSString *)fileName +{ + // A log file has a name like "log-.txt", where is a HEX-string of 6 characters. + // + // For example: log-DFFE99.txt + + BOOL hasProperPrefix = [fileName hasPrefix:@"log-"]; + + BOOL hasProperLength = [fileName length] >= 10; + + + if (hasProperPrefix && hasProperLength) + { + NSCharacterSet *hexSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"]; + + NSString *hex = [fileName substringWithRange:NSMakeRange(4, 6)]; + NSString *nohex = [hex stringByTrimmingCharactersInSet:hexSet]; + + if ([nohex length] == 0) + { + return YES; + } + } + + return NO; +} + +/** + * Returns an array of NSString objects, + * each of which is the filePath to an existing log file on disk. +**/ +- (NSArray *)unsortedLogFilePaths +{ + NSString *logsDirectory = [self logsDirectory]; + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil]; + + NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]]; + + for (NSString *fileName in fileNames) + { + // Filter out any files that aren't log files. (Just for extra safety) + + if ([self isLogFile:fileName]) + { + NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; + + [unsortedLogFilePaths addObject:filePath]; + } + } + + return unsortedLogFilePaths; +} + +/** + * Returns an array of NSString objects, + * each of which is the fileName of an existing log file on disk. +**/ +- (NSArray *)unsortedLogFileNames +{ + NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; + + NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; + + for (NSString *filePath in unsortedLogFilePaths) + { + [unsortedLogFileNames addObject:[filePath lastPathComponent]]; + } + + return unsortedLogFileNames; +} + +/** + * Returns an array of DDLogFileInfo objects, + * each representing an existing log file on disk, + * and containing important information about the log file such as it's modification date and size. +**/ +- (NSArray *)unsortedLogFileInfos +{ + NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; + + NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; + + for (NSString *filePath in unsortedLogFilePaths) + { + DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath]; + + [unsortedLogFileInfos addObject:logFileInfo]; + } + + return unsortedLogFileInfos; +} + +/** + * Just like the unsortedLogFilePaths method, but sorts the array. + * The items in the array are sorted by modification date. + * The first item in the array will be the most recently modified log file. +**/ +- (NSArray *)sortedLogFilePaths +{ + NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; + + NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]]; + + for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) + { + [sortedLogFilePaths addObject:[logFileInfo filePath]]; + } + + return sortedLogFilePaths; +} + +/** + * Just like the unsortedLogFileNames method, but sorts the array. + * The items in the array are sorted by modification date. + * The first item in the array will be the most recently modified log file. +**/ +- (NSArray *)sortedLogFileNames +{ + NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; + + NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]]; + + for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) + { + [sortedLogFileNames addObject:[logFileInfo fileName]]; + } + + return sortedLogFileNames; +} + +/** + * Just like the unsortedLogFileInfos method, but sorts the array. + * The items in the array are sorted by modification date. + * The first item in the array will be the most recently modified log file. +**/ +- (NSArray *)sortedLogFileInfos +{ + return [[self unsortedLogFileInfos] sortedArrayUsingSelector:@selector(reverseCompareByCreationDate:)]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Creation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Generates a short UUID suitable for use in the log file's name. + * The result will have six characters, all in the hexadecimal set [0123456789ABCDEF]. +**/ +- (NSString *)generateShortUUID +{ + CFUUIDRef uuid = CFUUIDCreate(NULL); + + CFStringRef fullStr = CFUUIDCreateString(NULL, uuid); + NSString *result = (__bridge_transfer NSString *)CFStringCreateWithSubstring(NULL, fullStr, CFRangeMake(0, 6)); + + CFRelease(fullStr); + CFRelease(uuid); + + return result; +} + +/** + * Generates a new unique log file path, and creates the corresponding log file. +**/ +- (NSString *)createNewLogFile +{ + // Generate a random log file name, and create the file (if there isn't a collision) + + NSString *logsDirectory = [self logsDirectory]; + do + { + NSString *fileName = [NSString stringWithFormat:@"log-%@.txt", [self generateShortUUID]]; + + NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) + { + NSLogVerbose(@"DDLogFileManagerDefault: Creating new log file: %@", fileName); + + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + // Since we just created a new log file, we may need to delete some old log files + [self deleteOldLogFiles]; + + return filePath; + } + + } while(YES); +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLogFileFormatterDefault + +- (id)init +{ + return [self initWithDateFormatter:nil]; +} + +- (id)initWithDateFormatter:(NSDateFormatter *)aDateFormatter +{ + if ((self = [super init])) + { + if (aDateFormatter) + { + dateFormatter = aDateFormatter; + } + else + { + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style + [dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"]; + } + } + return self; +} + +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage +{ + NSString *dateAndTime = [dateFormatter stringFromDate:(logMessage->timestamp)]; + + return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->logMsg]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDFileLogger + +- (id)init +{ + DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init]; + + return [self initWithLogFileManager:defaultLogFileManager]; +} + +- (id)initWithLogFileManager:(id )aLogFileManager +{ + if ((self = [super init])) + { + maximumFileSize = DEFAULT_LOG_MAX_FILE_SIZE; + rollingFrequency = DEFAULT_LOG_ROLLING_FREQUENCY; + + logFileManager = aLogFileManager; + + formatter = [[DDLogFileFormatterDefault alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [currentLogFileHandle synchronizeFile]; + [currentLogFileHandle closeFile]; + + if (rollingTimer) + { + dispatch_source_cancel(rollingTimer); + rollingTimer = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize logFileManager; + +- (unsigned long long)maximumFileSize +{ + __block unsigned long long result; + + dispatch_block_t block = ^{ + result = maximumFileSize; + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the maximumFileSize variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, block); + }); + + return result; +} + +- (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize +{ + dispatch_block_t block = ^{ @autoreleasepool { + + maximumFileSize = newMaximumFileSize; + [self maybeRollLogFileDueToSize]; + + }}; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the maximumFileSize variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); +} + +- (NSTimeInterval)rollingFrequency +{ + __block NSTimeInterval result; + + dispatch_block_t block = ^{ + result = rollingFrequency; + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation should access the rollingFrequency variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, block); + }); + + return result; +} + +- (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency +{ + dispatch_block_t block = ^{ @autoreleasepool { + + rollingFrequency = newRollingFrequency; + [self maybeRollLogFileDueToAge]; + }}; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation should access the rollingFrequency variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark File Rolling +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)scheduleTimerToRollLogFileDueToAge +{ + if (rollingTimer) + { + dispatch_source_cancel(rollingTimer); + rollingTimer = NULL; + } + + if (currentLogFileInfo == nil || rollingFrequency <= 0.0) + { + return; + } + + NSDate *logFileCreationDate = [currentLogFileInfo creationDate]; + + NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate]; + ti += rollingFrequency; + + NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti]; + + NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge"); + + NSLogVerbose(@"DDFileLogger: logFileCreationDate: %@", logFileCreationDate); + NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate); + + rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + dispatch_source_set_event_handler(rollingTimer, ^{ @autoreleasepool { + + [self maybeRollLogFileDueToAge]; + + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theRollingTimer = rollingTimer; + dispatch_source_set_cancel_handler(rollingTimer, ^{ + dispatch_release(theRollingTimer); + }); + #endif + + uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * NSEC_PER_SEC); + dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay); + + dispatch_source_set_timer(rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1.0); + dispatch_resume(rollingTimer); +} + +- (void)rollLogFile +{ + // This method is public. + // We need to execute the rolling on our logging thread/queue. + + dispatch_block_t block = ^{ @autoreleasepool { + + [self rollLogFileNow]; + }}; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)rollLogFileNow +{ + NSLogVerbose(@"DDFileLogger: rollLogFileNow"); + + + if (currentLogFileHandle == nil) return; + + [currentLogFileHandle synchronizeFile]; + [currentLogFileHandle closeFile]; + currentLogFileHandle = nil; + + currentLogFileInfo.isArchived = YES; + + if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) + { + [logFileManager didRollAndArchiveLogFile:(currentLogFileInfo.filePath)]; + } + + currentLogFileInfo = nil; + + if (rollingTimer) + { + dispatch_source_cancel(rollingTimer); + rollingTimer = NULL; + } +} + +- (void)maybeRollLogFileDueToAge +{ + if (rollingFrequency > 0.0 && currentLogFileInfo.age >= rollingFrequency) + { + NSLogVerbose(@"DDFileLogger: Rolling log file due to age..."); + + [self rollLogFileNow]; + } + else + { + [self scheduleTimerToRollLogFileDueToAge]; + } +} + +- (void)maybeRollLogFileDueToSize +{ + // This method is called from logMessage. + // Keep it FAST. + + // Note: Use direct access to maximumFileSize variable. + // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons). + + if (maximumFileSize > 0) + { + unsigned long long fileSize = [currentLogFileHandle offsetInFile]; + + if (fileSize >= maximumFileSize) + { + NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize); + + [self rollLogFileNow]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark File Logging +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the log file that should be used. + * If there is an existing log file that is suitable, + * within the constraints of maximumFileSize and rollingFrequency, then it is returned. + * + * Otherwise a new file is created and returned. +**/ +- (DDLogFileInfo *)currentLogFileInfo +{ + if (currentLogFileInfo == nil) + { + NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos]; + + if ([sortedLogFileInfos count] > 0) + { + DDLogFileInfo *mostRecentLogFileInfo = [sortedLogFileInfos objectAtIndex:0]; + + BOOL useExistingLogFile = YES; + BOOL shouldArchiveMostRecent = NO; + + if (mostRecentLogFileInfo.isArchived) + { + useExistingLogFile = NO; + shouldArchiveMostRecent = NO; + } + else if (maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= maximumFileSize) + { + useExistingLogFile = NO; + shouldArchiveMostRecent = YES; + } + else if (rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= rollingFrequency) + { + useExistingLogFile = NO; + shouldArchiveMostRecent = YES; + } + + if (useExistingLogFile) + { + NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", mostRecentLogFileInfo.fileName); + + currentLogFileInfo = mostRecentLogFileInfo; + } + else + { + if (shouldArchiveMostRecent) + { + mostRecentLogFileInfo.isArchived = YES; + + if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) + { + [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)]; + } + } + } + } + + if (currentLogFileInfo == nil) + { + NSString *currentLogFilePath = [logFileManager createNewLogFile]; + + currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath]; + } + } + + return currentLogFileInfo; +} + +- (NSFileHandle *)currentLogFileHandle +{ + if (currentLogFileHandle == nil) + { + NSString *logFilePath = [[self currentLogFileInfo] filePath]; + + currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; + [currentLogFileHandle seekToEndOfFile]; + + if (currentLogFileHandle) + { + [self scheduleTimerToRollLogFileDueToAge]; + } + } + + return currentLogFileHandle; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)logMessage:(DDLogMessage *)logMessage +{ + NSString *logMsg = logMessage->logMsg; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + } + + if (logMsg) + { + if (![logMsg hasSuffix:@"\n"]) + { + logMsg = [logMsg stringByAppendingString:@"\n"]; + } + + NSData *logData = [logMsg dataUsingEncoding:NSUTF8StringEncoding]; + + [[self currentLogFileHandle] writeData:logData]; + + [self maybeRollLogFileDueToSize]; + } +} + +- (void)willRemoveLogger +{ + // If you override me be sure to invoke [super willRemoveLogger]; + + [self rollLogFileNow]; +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.fileLogger"; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_IPHONE_SIMULATOR + #define XATTR_ARCHIVED_NAME @"archived" +#else + #define XATTR_ARCHIVED_NAME @"lumberjack.log.archived" +#endif + +@implementation DDLogFileInfo + +@synthesize filePath; + +@dynamic fileName; +@dynamic fileAttributes; +@dynamic creationDate; +@dynamic modificationDate; +@dynamic fileSize; +@dynamic age; + +@dynamic isArchived; + + +#pragma mark Lifecycle + ++ (id)logFileWithPath:(NSString *)aFilePath +{ + return [[DDLogFileInfo alloc] initWithFilePath:aFilePath]; +} + +- (id)initWithFilePath:(NSString *)aFilePath +{ + if ((self = [super init])) + { + filePath = [aFilePath copy]; + } + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Standard Info +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSDictionary *)fileAttributes +{ + if (fileAttributes == nil) + { + fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; + } + return fileAttributes; +} + +- (NSString *)fileName +{ + if (fileName == nil) + { + fileName = [filePath lastPathComponent]; + } + return fileName; +} + +- (NSDate *)modificationDate +{ + if (modificationDate == nil) + { + modificationDate = [[self fileAttributes] objectForKey:NSFileModificationDate]; + } + + return modificationDate; +} + +- (NSDate *)creationDate +{ + if (creationDate == nil) + { + + #if TARGET_OS_IPHONE + + const char *path = [filePath UTF8String]; + + struct attrlist attrList; + memset(&attrList, 0, sizeof(attrList)); + attrList.bitmapcount = ATTR_BIT_MAP_COUNT; + attrList.commonattr = ATTR_CMN_CRTIME; + + struct { + u_int32_t attrBufferSizeInBytes; + struct timespec crtime; + } attrBuffer; + + int result = getattrlist(path, &attrList, &attrBuffer, sizeof(attrBuffer), 0); + if (result == 0) + { + double seconds = (double)(attrBuffer.crtime.tv_sec); + double nanos = (double)(attrBuffer.crtime.tv_nsec); + + NSTimeInterval ti = seconds + (nanos / 1000000000.0); + + creationDate = [NSDate dateWithTimeIntervalSince1970:ti]; + } + else + { + NSLogError(@"DDLogFileInfo: creationDate(%@): getattrlist result = %i", self.fileName, result); + } + + #else + + creationDate = [[self fileAttributes] objectForKey:NSFileCreationDate]; + + #endif + + } + return creationDate; +} + +- (unsigned long long)fileSize +{ + if (fileSize == 0) + { + fileSize = [[[self fileAttributes] objectForKey:NSFileSize] unsignedLongLongValue]; + } + + return fileSize; +} + +- (NSTimeInterval)age +{ + return [[self creationDate] timeIntervalSinceNow] * -1.0; +} + +- (NSString *)description +{ + return [@{@"filePath": self.filePath, + @"fileName": self.fileName, + @"fileAttributes": self.fileAttributes, + @"creationDate": self.creationDate, + @"modificationDate": self.modificationDate, + @"fileSize": @(self.fileSize), + @"age": @(self.age), + @"isArchived": @(self.isArchived)} description]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Archiving +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isArchived +{ + +#if TARGET_IPHONE_SIMULATOR + + // Extended attributes don't work properly on the simulator. + // So we have to use a less attractive alternative. + // See full explanation in the header file. + + return [self hasExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; + +#else + + return [self hasExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; + +#endif +} + +- (void)setIsArchived:(BOOL)flag +{ + +#if TARGET_IPHONE_SIMULATOR + + // Extended attributes don't work properly on the simulator. + // So we have to use a less attractive alternative. + // See full explanation in the header file. + + if (flag) + [self addExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; + else + [self removeExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; + +#else + + if (flag) + [self addExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; + else + [self removeExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; + +#endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Changes +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)reset +{ + fileName = nil; + fileAttributes = nil; + creationDate = nil; + modificationDate = nil; +} + +- (void)renameFile:(NSString *)newFileName +{ + // This method is only used on the iPhone simulator, where normal extended attributes are broken. + // See full explanation in the header file. + + if (![newFileName isEqualToString:[self fileName]]) + { + NSString *fileDir = [filePath stringByDeletingLastPathComponent]; + + NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName]; + + NSLogVerbose(@"DDLogFileInfo: Renaming file: '%@' -> '%@'", self.fileName, newFileName); + + NSError *error = nil; + if (![[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newFilePath error:&error]) + { + NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error); + } + + filePath = newFilePath; + [self reset]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Attribute Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_IPHONE_SIMULATOR + +// Extended attributes don't work properly on the simulator. +// So we have to use a less attractive alternative. +// See full explanation in the header file. + +- (BOOL)hasExtensionAttributeWithName:(NSString *)attrName +{ + // This method is only used on the iPhone simulator, where normal extended attributes are broken. + // See full explanation in the header file. + + // Split the file name into components. + // + // log-ABC123.archived.uploaded.txt + // + // 0. log-ABC123 + // 1. archived + // 2. uploaded + // 3. txt + // + // So we want to search for the attrName in the components (ignoring the first and last array indexes). + + NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; + + // Watch out for file names without an extension + + NSUInteger count = [components count]; + NSUInteger max = (count >= 2) ? count-1 : count; + + NSUInteger i; + for (i = 1; i < max; i++) + { + NSString *attr = [components objectAtIndex:i]; + + if ([attrName isEqualToString:attr]) + { + return YES; + } + } + + return NO; +} + +- (void)addExtensionAttributeWithName:(NSString *)attrName +{ + // This method is only used on the iPhone simulator, where normal extended attributes are broken. + // See full explanation in the header file. + + if ([attrName length] == 0) return; + + // Example: + // attrName = "archived" + // + // "log-ABC123.txt" -> "log-ABC123.archived.txt" + + NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; + + NSUInteger count = [components count]; + + NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1; + NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; + + if (count > 0) + { + [newFileName appendString:[components objectAtIndex:0]]; + } + + NSString *lastExt = @""; + + NSUInteger i; + for (i = 1; i < count; i++) + { + NSString *attr = [components objectAtIndex:i]; + if ([attr length] == 0) + { + continue; + } + + if ([attrName isEqualToString:attr]) + { + // Extension attribute already exists in file name + return; + } + + if ([lastExt length] > 0) + { + [newFileName appendFormat:@".%@", lastExt]; + } + + lastExt = attr; + } + + [newFileName appendFormat:@".%@", attrName]; + + if ([lastExt length] > 0) + { + [newFileName appendFormat:@".%@", lastExt]; + } + + [self renameFile:newFileName]; +} + +- (void)removeExtensionAttributeWithName:(NSString *)attrName +{ + // This method is only used on the iPhone simulator, where normal extended attributes are broken. + // See full explanation in the header file. + + if ([attrName length] == 0) return; + + // Example: + // attrName = "archived" + // + // "log-ABC123.txt" -> "log-ABC123.archived.txt" + + NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; + + NSUInteger count = [components count]; + + NSUInteger estimatedNewLength = [[self fileName] length]; + NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; + + if (count > 0) + { + [newFileName appendString:[components objectAtIndex:0]]; + } + + BOOL found = NO; + + NSUInteger i; + for (i = 1; i < count; i++) + { + NSString *attr = [components objectAtIndex:i]; + + if ([attrName isEqualToString:attr]) + { + found = YES; + } + else + { + [newFileName appendFormat:@".%@", attr]; + } + } + + if (found) + { + [self renameFile:newFileName]; + } +} + +#else + +- (BOOL)hasExtendedAttributeWithName:(NSString *)attrName +{ + const char *path = [filePath UTF8String]; + const char *name = [attrName UTF8String]; + + ssize_t result = getxattr(path, name, NULL, 0, 0, 0); + + return (result >= 0); +} + +- (void)addExtendedAttributeWithName:(NSString *)attrName +{ + const char *path = [filePath UTF8String]; + const char *name = [attrName UTF8String]; + + int result = setxattr(path, name, NULL, 0, 0, 0); + + if (result < 0) + { + NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %i", attrName, self.fileName, result); + } +} + +- (void)removeExtendedAttributeWithName:(NSString *)attrName +{ + const char *path = [filePath UTF8String]; + const char *name = [attrName UTF8String]; + + int result = removexattr(path, name, 0); + + if (result < 0 && errno != ENOATTR) + { + NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %i", attrName, self.fileName, result); + } +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparisons +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[self class]]) + { + DDLogFileInfo *another = (DDLogFileInfo *)object; + + return [filePath isEqualToString:[another filePath]]; + } + + return NO; +} + +- (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another +{ + NSDate *us = [self creationDate]; + NSDate *them = [another creationDate]; + + NSComparisonResult result = [us compare:them]; + + if (result == NSOrderedAscending) + return NSOrderedDescending; + + if (result == NSOrderedDescending) + return NSOrderedAscending; + + return NSOrderedSame; +} + +- (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another +{ + NSDate *us = [self modificationDate]; + NSDate *them = [another modificationDate]; + + NSComparisonResult result = [us compare:them]; + + if (result == NSOrderedAscending) + return NSOrderedDescending; + + if (result == NSOrderedDescending) + return NSOrderedAscending; + + return NSOrderedSame; +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDLog.h b/msext/Class/http/CocoaLumberjack/DDLog.h new file mode 100755 index 0000000..5da1849 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDLog.h @@ -0,0 +1,601 @@ +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * Otherwise, here is a quick refresher. + * There are three steps to using the macros: + * + * Step 1: + * Import the header in your implementation file: + * + * #import "DDLog.h" + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const int ddLogLevel = LOG_LEVEL_VERBOSE; + * + * Step 3: + * Replace your NSLog statements with DDLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> DDLogError(@"Fatal error, no dohickey found!"); + * + * DDLog works exactly the same as NSLog. + * This means you can pass it multiple variables just like NSLog. +**/ + + +@class DDLogMessage; + +@protocol DDLogger; +@protocol DDLogFormatter; + +/** + * This is the single macro that all other macros below compile into. + * This big multiline macro makes all the other macros easier to read. +**/ + +#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \ + [DDLog log:isAsynchronous \ + level:lvl \ + flag:flg \ + context:ctx \ + file:__FILE__ \ + function:fnct \ + line:__LINE__ \ + tag:atag \ + format:(frmt), ##__VA_ARGS__] + +/** + * Define the Objective-C and C versions of the macro. + * These automatically inject the proper function name for either an objective-c method or c function. + * + * We also define shorthand versions for asynchronous and synchronous logging. +**/ + +#define LOG_OBJC_MACRO(async, lvl, flg, ctx, frmt, ...) \ + LOG_MACRO(async, lvl, flg, ctx, nil, sel_getName(_cmd), frmt, ##__VA_ARGS__) + +#define LOG_C_MACRO(async, lvl, flg, ctx, frmt, ...) \ + LOG_MACRO(async, lvl, flg, ctx, nil, __FUNCTION__, frmt, ##__VA_ARGS__) + +#define SYNC_LOG_OBJC_MACRO(lvl, flg, ctx, frmt, ...) \ + LOG_OBJC_MACRO( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +#define ASYNC_LOG_OBJC_MACRO(lvl, flg, ctx, frmt, ...) \ + LOG_OBJC_MACRO(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +#define SYNC_LOG_C_MACRO(lvl, flg, ctx, frmt, ...) \ + LOG_C_MACRO( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +#define ASYNC_LOG_C_MACRO(lvl, flg, ctx, frmt, ...) \ + LOG_C_MACRO(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +/** + * Define version of the macro that only execute if the logLevel is above the threshold. + * The compiled versions essentially look like this: + * + * if (logFlagForThisLogMsg & ddLogLevel) { execute log message } + * + * As shown further below, Lumberjack actually uses a bitmask as opposed to primitive log levels. + * This allows for a great amount of flexibility and some pretty advanced fine grained logging techniques. + * + * Note that when compiler optimizations are enabled (as they are for your release builds), + * the log messages above your logging threshold will automatically be compiled out. + * + * (If the compiler sees ddLogLevel declared as a constant, the compiler simply checks to see if the 'if' statement + * would execute, and if not it strips it from the binary.) + * + * We also define shorthand versions for asynchronous and synchronous logging. +**/ + +#define LOG_MAYBE(async, lvl, flg, ctx, fnct, frmt, ...) \ + do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, nil, fnct, frmt, ##__VA_ARGS__); } while(0) + +#define LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \ + LOG_MAYBE(async, lvl, flg, ctx, sel_getName(_cmd), frmt, ##__VA_ARGS__) + +#define LOG_C_MAYBE(async, lvl, flg, ctx, frmt, ...) \ + LOG_MAYBE(async, lvl, flg, ctx, __FUNCTION__, frmt, ##__VA_ARGS__) + +#define SYNC_LOG_OBJC_MAYBE(lvl, flg, ctx, frmt, ...) \ + LOG_OBJC_MAYBE( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +#define ASYNC_LOG_OBJC_MAYBE(lvl, flg, ctx, frmt, ...) \ + LOG_OBJC_MAYBE(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +#define SYNC_LOG_C_MAYBE(lvl, flg, ctx, frmt, ...) \ + LOG_C_MAYBE( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +#define ASYNC_LOG_C_MAYBE(lvl, flg, ctx, frmt, ...) \ + LOG_C_MAYBE(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__) + +/** + * Define versions of the macros that also accept tags. + * + * The DDLogMessage object includes a 'tag' ivar that may be used for a variety of purposes. + * It may be used to pass custom information to loggers or formatters. + * Or it may be used by 3rd party extensions to the framework. + * + * Thes macros just make it a little easier to extend logging functionality. +**/ + +#define LOG_OBJC_TAG_MACRO(async, lvl, flg, ctx, tag, frmt, ...) \ + LOG_MACRO(async, lvl, flg, ctx, tag, sel_getName(_cmd), frmt, ##__VA_ARGS__) + +#define LOG_C_TAG_MACRO(async, lvl, flg, ctx, tag, frmt, ...) \ + LOG_MACRO(async, lvl, flg, ctx, tag, __FUNCTION__, frmt, ##__VA_ARGS__) + +#define LOG_TAG_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, ...) \ + do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, ##__VA_ARGS__); } while(0) + +#define LOG_OBJC_TAG_MAYBE(async, lvl, flg, ctx, tag, frmt, ...) \ + LOG_TAG_MAYBE(async, lvl, flg, ctx, tag, sel_getName(_cmd), frmt, ##__VA_ARGS__) + +#define LOG_C_TAG_MAYBE(async, lvl, flg, ctx, tag, frmt, ...) \ + LOG_TAG_MAYBE(async, lvl, flg, ctx, tag, __FUNCTION__, frmt, ##__VA_ARGS__) + +/** + * Define the standard options. + * + * We default to only 4 levels because it makes it easier for beginners + * to make the transition to a logging framework. + * + * More advanced users may choose to completely customize the levels (and level names) to suite their needs. + * For more information on this see the "Custom Log Levels" page: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomLogLevels + * + * Advanced users may also notice that we're using a bitmask. + * This is to allow for custom fine grained logging: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/FineGrainedLogging + * + * -- Flags -- + * + * Typically you will use the LOG_LEVELS (see below), but the flags may be used directly in certain situations. + * For example, say you have a lot of warning log messages, and you wanted to disable them. + * However, you still needed to see your error and info log messages. + * You could accomplish that with the following: + * + * static const int ddLogLevel = LOG_FLAG_ERROR | LOG_FLAG_INFO; + * + * Flags may also be consulted when writing custom log formatters, + * as the DDLogMessage class captures the individual flag that caused the log message to fire. + * + * -- Levels -- + * + * Log levels are simply the proper bitmask of the flags. + * + * -- Booleans -- + * + * The booleans may be used when your logging code involves more than one line. + * For example: + * + * if (LOG_VERBOSE) { + * for (id sprocket in sprockets) + * DDLogVerbose(@"sprocket: %@", [sprocket description]) + * } + * + * -- Async -- + * + * Defines the default asynchronous options. + * The default philosophy for asynchronous logging is very simple: + * + * Log messages with errors should be executed synchronously. + * After all, an error just occurred. The application could be unstable. + * + * All other log messages, such as debug output, are executed asynchronously. + * After all, if it wasn't an error, then it was just informational output, + * or something the application was easily able to recover from. + * + * -- Changes -- + * + * You are strongly discouraged from modifying this file. + * If you do, you make it more difficult on yourself to merge future bug fixes and improvements from the project. + * Instead, create your own MyLogging.h or ApplicationNameLogging.h or CompanyLogging.h + * + * For an example of customizing your logging experience, see the "Custom Log Levels" page: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomLogLevels +**/ + +#define LOG_FLAG_ERROR (1 << 0) // 0...0001 +#define LOG_FLAG_WARN (1 << 1) // 0...0010 +#define LOG_FLAG_INFO (1 << 2) // 0...0100 +#define LOG_FLAG_VERBOSE (1 << 3) // 0...1000 + +#define LOG_LEVEL_OFF 0 +#define LOG_LEVEL_ERROR (LOG_FLAG_ERROR) // 0...0001 +#define LOG_LEVEL_WARN (LOG_FLAG_ERROR | LOG_FLAG_WARN) // 0...0011 +#define LOG_LEVEL_INFO (LOG_FLAG_ERROR | LOG_FLAG_WARN | LOG_FLAG_INFO) // 0...0111 +#define LOG_LEVEL_VERBOSE (LOG_FLAG_ERROR | LOG_FLAG_WARN | LOG_FLAG_INFO | LOG_FLAG_VERBOSE) // 0...1111 + +#define LOG_ERROR (ddLogLevel & LOG_FLAG_ERROR) +#define LOG_WARN (ddLogLevel & LOG_FLAG_WARN) +#define LOG_INFO (ddLogLevel & LOG_FLAG_INFO) +#define LOG_VERBOSE (ddLogLevel & LOG_FLAG_VERBOSE) + +#define LOG_ASYNC_ENABLED YES + +#define LOG_ASYNC_ERROR ( NO && LOG_ASYNC_ENABLED) +#define LOG_ASYNC_WARN (YES && LOG_ASYNC_ENABLED) +#define LOG_ASYNC_INFO (YES && LOG_ASYNC_ENABLED) +#define LOG_ASYNC_VERBOSE (YES && LOG_ASYNC_ENABLED) + +#define DDLogError(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_ERROR, ddLogLevel, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__) +#define DDLogWarn(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_WARN, ddLogLevel, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__) +#define DDLogInfo(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_INFO, ddLogLevel, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__) +#define DDLogVerbose(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, ddLogLevel, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__) + +#define DDLogCError(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_ERROR, ddLogLevel, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__) +#define DDLogCWarn(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_WARN, ddLogLevel, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__) +#define DDLogCInfo(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_INFO, ddLogLevel, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__) +#define DDLogCVerbose(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_VERBOSE, ddLogLevel, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__) + +/** + * The THIS_FILE macro gives you an NSString of the file name. + * For simplicity and clarity, the file name does not include the full path or file extension. + * + * For example: DDLogWarn(@"%@: Unable to find thingy", THIS_FILE) -> @"MyViewController: Unable to find thingy" +**/ + +NSString *DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy); + +#define THIS_FILE (DDExtractFileNameWithoutExtension(__FILE__, NO)) + +/** + * The THIS_METHOD macro gives you the name of the current objective-c method. + * + * For example: DDLogWarn(@"%@ - Requires non-nil strings", THIS_METHOD) -> @"setMake:model: requires non-nil strings" + * + * Note: This does NOT work in straight C functions (non objective-c). + * Instead you should use the predefined __FUNCTION__ macro. +**/ + +#define THIS_METHOD NSStringFromSelector(_cmd) + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface DDLog : NSObject + +/** + * Provides access to the underlying logging queue. + * This may be helpful to Logger classes for things like thread synchronization. +**/ + ++ (dispatch_queue_t)loggingQueue; + +/** + * Logging Primitive. + * + * This method is used by the macros above. + * It is suggested you stick with the macros as they're easier to use. +**/ + ++ (void)log:(BOOL)synchronous + level:(int)level + flag:(int)flag + context:(int)context + file:(const char *)file + function:(const char *)function + line:(int)line + tag:(id)tag + format:(NSString *)format, ... __attribute__ ((format (__NSString__, 9, 10))); + +/** + * Logging Primitive. + * + * This method can be used if you have a prepared va_list. +**/ + ++ (void)log:(BOOL)asynchronous + level:(int)level + flag:(int)flag + context:(int)context + file:(const char *)file + function:(const char *)function + line:(int)line + tag:(id)tag + format:(NSString *)format + args:(va_list)argList; + + +/** + * Since logging can be asynchronous, there may be times when you want to flush the logs. + * The framework invokes this automatically when the application quits. +**/ + ++ (void)flushLog; + +/** + * Loggers + * + * If you want your log statements to go somewhere, + * you should create and add a logger. +**/ + ++ (void)addLogger:(id )logger; ++ (void)removeLogger:(id )logger; + ++ (void)removeAllLoggers; + +/** + * Registered Dynamic Logging + * + * These methods allow you to obtain a list of classes that are using registered dynamic logging, + * and also provides methods to get and set their log level during run time. +**/ + ++ (NSArray *)registeredClasses; ++ (NSArray *)registeredClassNames; + ++ (int)logLevelForClass:(Class)aClass; ++ (int)logLevelForClassWithName:(NSString *)aClassName; + ++ (void)setLogLevel:(int)logLevel forClass:(Class)aClass; ++ (void)setLogLevel:(int)logLevel forClassWithName:(NSString *)aClassName; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol DDLogger +@required + +- (void)logMessage:(DDLogMessage *)logMessage; + +/** + * Formatters may optionally be added to any logger. + * + * If no formatter is set, the logger simply logs the message as it is given in logMessage, + * or it may use its own built in formatting style. +**/ +- (id )logFormatter; +- (void)setLogFormatter:(id )formatter; + +@optional + +/** + * Since logging is asynchronous, adding and removing loggers is also asynchronous. + * In other words, the loggers are added and removed at appropriate times with regards to log messages. + * + * - Loggers will not receive log messages that were executed prior to when they were added. + * - Loggers will not receive log messages that were executed after they were removed. + * + * These methods are executed in the logging thread/queue. + * This is the same thread/queue that will execute every logMessage: invocation. + * Loggers may use these methods for thread synchronization or other setup/teardown tasks. +**/ +- (void)didAddLogger; +- (void)willRemoveLogger; + +/** + * Some loggers may buffer IO for optimization purposes. + * For example, a database logger may only save occasionaly as the disk IO is slow. + * In such loggers, this method should be implemented to flush any pending IO. + * + * This allows invocations of DDLog's flushLog method to be propogated to loggers that need it. + * + * Note that DDLog's flushLog method is invoked automatically when the application quits, + * and it may be also invoked manually by the developer prior to application crashes, or other such reasons. +**/ +- (void)flush; + +/** + * Each logger is executed concurrently with respect to the other loggers. + * Thus, a dedicated dispatch queue is used for each logger. + * Logger implementations may optionally choose to provide their own dispatch queue. +**/ +- (dispatch_queue_t)loggerQueue; + +/** + * If the logger implementation does not choose to provide its own queue, + * one will automatically be created for it. + * The created queue will receive its name from this method. + * This may be helpful for debugging or profiling reasons. +**/ +- (NSString *)loggerName; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol DDLogFormatter +@required + +/** + * Formatters may optionally be added to any logger. + * This allows for increased flexibility in the logging environment. + * For example, log messages for log files may be formatted differently than log messages for the console. + * + * For more information about formatters, see the "Custom Formatters" page: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters + * + * The formatter may also optionally filter the log message by returning nil, + * in which case the logger will not log the message. +**/ +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage; + +@optional + +/** + * A single formatter instance can be added to multiple loggers. + * These methods provides hooks to notify the formatter of when it's added/removed. + * + * This is primarily for thread-safety. + * If a formatter is explicitly not thread-safe, it may wish to throw an exception if added to multiple loggers. + * Or if a formatter has potentially thread-unsafe code (e.g. NSDateFormatter), + * it could possibly use these hooks to switch to thread-safe versions of the code. +**/ +- (void)didAddToLogger:(id )logger; +- (void)willRemoveFromLogger:(id )logger; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol DDRegisteredDynamicLogging + +/** + * Implement these methods to allow a file's log level to be managed from a central location. + * + * This is useful if you'd like to be able to change log levels for various parts + * of your code from within the running application. + * + * Imagine pulling up the settings for your application, + * and being able to configure the logging level on a per file basis. + * + * The implementation can be very straight-forward: + * + * + (int)ddLogLevel + * { + * return ddLogLevel; + * } + * + * + (void)ddSetLogLevel:(int)logLevel + * { + * ddLogLevel = logLevel; + * } +**/ + ++ (int)ddLogLevel; ++ (void)ddSetLogLevel:(int)logLevel; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The DDLogMessage class encapsulates information about the log message. + * If you write custom loggers or formatters, you will be dealing with objects of this class. +**/ + +enum { + DDLogMessageCopyFile = 1 << 0, + DDLogMessageCopyFunction = 1 << 1 +}; +typedef int DDLogMessageOptions; + +@interface DDLogMessage : NSObject +{ + +// The public variables below can be accessed directly (for speed). +// For example: logMessage->logLevel + +@public + int logLevel; + int logFlag; + int logContext; + NSString *logMsg; + NSDate *timestamp; + char *file; + char *function; + int lineNumber; + mach_port_t machThreadID; + char *queueLabel; + NSString *threadName; + + // For 3rd party extensions to the framework, where flags and contexts aren't enough. + id tag; + + // For 3rd party extensions that manually create DDLogMessage instances. + DDLogMessageOptions options; +} + +/** + * Standard init method for a log message object. + * Used by the logging primitives. (And the macros use the logging primitives.) + * + * If you find need to manually create logMessage objects, there is one thing you should be aware of: + * + * If no flags are passed, the method expects the file and function parameters to be string literals. + * That is, it expects the given strings to exist for the duration of the object's lifetime, + * and it expects the given strings to be immutable. + * In other words, it does not copy these strings, it simply points to them. + * This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters, + * so it makes sense to optimize and skip the unnecessary allocations. + * However, if you need them to be copied you may use the options parameter to specify this. + * Options is a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction. +**/ +- (id)initWithLogMsg:(NSString *)logMsg + level:(int)logLevel + flag:(int)logFlag + context:(int)logContext + file:(const char *)file + function:(const char *)function + line:(int)line + tag:(id)tag + options:(DDLogMessageOptions)optionsMask; + +/** + * Returns the threadID as it appears in NSLog. + * That is, it is a hexadecimal value which is calculated from the machThreadID. +**/ +- (NSString *)threadID; + +/** + * Convenience property to get just the file name, as the file variable is generally the full file path. + * This method does not include the file extension, which is generally unwanted for logging purposes. +**/ +- (NSString *)fileName; + +/** + * Returns the function variable in NSString form. +**/ +- (NSString *)methodName; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The DDLogger protocol specifies that an optional formatter can be added to a logger. + * Most (but not all) loggers will want to support formatters. + * + * However, writting getters and setters in a thread safe manner, + * while still maintaining maximum speed for the logging process, is a difficult task. + * + * To do it right, the implementation of the getter/setter has strict requiremenets: + * - Must NOT require the logMessage method to acquire a lock. + * - Must NOT require the logMessage method to access an atomic property (also a lock of sorts). + * + * To simplify things, an abstract logger is provided that implements the getter and setter. + * + * Logger implementations may simply extend this class, + * and they can ACCESS THE FORMATTER VARIABLE DIRECTLY from within their logMessage method! +**/ + +@interface DDAbstractLogger : NSObject +{ + id formatter; + + dispatch_queue_t loggerQueue; +} + +- (id )logFormatter; +- (void)setLogFormatter:(id )formatter; + +// For thread-safety assertions +- (BOOL)isOnGlobalLoggingQueue; +- (BOOL)isOnInternalLoggerQueue; + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDLog.m b/msext/Class/http/CocoaLumberjack/DDLog.m new file mode 100755 index 0000000..1e1ddd6 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDLog.m @@ -0,0 +1,1083 @@ +#import "DDLog.h" + +#import +#import +#import +#import +#import + + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// We probably shouldn't be using DDLog() statements within the DDLog implementation. +// But we still want to leave our log statements for any future debugging, +// and to allow other developers to trace the implementation (which is a great learning tool). +// +// So we use a primitive logging macro around NSLog. +// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. + +#define DD_DEBUG NO + +#define NSLogDebug(frmt, ...) do{ if(DD_DEBUG) NSLog((frmt), ##__VA_ARGS__); } while(0) + +// Specifies the maximum queue size of the logging thread. +// +// Since most logging is asynchronous, its possible for rogue threads to flood the logging queue. +// That is, to issue an abundance of log statements faster than the logging thread can keepup. +// Typically such a scenario occurs when log statements are added haphazardly within large loops, +// but may also be possible if relatively slow loggers are being used. +// +// This property caps the queue size at a given number of outstanding log statements. +// If a thread attempts to issue a log statement when the queue is already maxed out, +// the issuing thread will block until the queue size drops below the max again. + +#define LOG_MAX_QUEUE_SIZE 1000 // Should not exceed INT32_MAX + +// The "global logging queue" refers to [DDLog loggingQueue]. +// It is the queue that all log statements go through. +// +// The logging queue sets a flag via dispatch_queue_set_specific using this key. +// We can check for this key via dispatch_get_specific() to see if we're on the "global logging queue". + +static void *const GlobalLoggingQueueIdentityKey = (void *)&GlobalLoggingQueueIdentityKey; + + +@interface DDLoggerNode : NSObject { +@public + id logger; + dispatch_queue_t loggerQueue; +} + ++ (DDLoggerNode *)nodeWithLogger:(id )logger loggerQueue:(dispatch_queue_t)loggerQueue; + +@end + + +@interface DDLog (PrivateAPI) + ++ (void)lt_addLogger:(id )logger; ++ (void)lt_removeLogger:(id )logger; ++ (void)lt_removeAllLoggers; ++ (void)lt_log:(DDLogMessage *)logMessage; ++ (void)lt_flush; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLog + +// An array used to manage all the individual loggers. +// The array is only modified on the loggingQueue/loggingThread. +static NSMutableArray *loggers; + +// All logging statements are added to the same queue to ensure FIFO operation. +static dispatch_queue_t loggingQueue; + +// Individual loggers are executed concurrently per log statement. +// Each logger has it's own associated queue, and a dispatch group is used for synchrnoization. +static dispatch_group_t loggingGroup; + +// In order to prevent to queue from growing infinitely large, +// a maximum size is enforced (LOG_MAX_QUEUE_SIZE). +static dispatch_semaphore_t queueSemaphore; + +// Minor optimization for uniprocessor machines +static unsigned int numProcessors; + +/** + * The runtime sends initialize to each class in a program exactly one time just before the class, + * or any class that inherits from it, is sent its first message from within the program. (Thus the + * method may never be invoked if the class is not used.) The runtime sends the initialize message to + * classes in a thread-safe manner. Superclasses receive this message before their subclasses. + * + * This method may also be called directly (assumably by accident), hence the safety mechanism. +**/ ++ (void)initialize +{ + static BOOL initialized = NO; + if (!initialized) + { + initialized = YES; + + loggers = [[NSMutableArray alloc] initWithCapacity:4]; + + NSLogDebug(@"DDLog: Using grand central dispatch"); + + loggingQueue = dispatch_queue_create("cocoa.lumberjack", NULL); + loggingGroup = dispatch_group_create(); + + void *nonNullValue = GlobalLoggingQueueIdentityKey; // Whatever, just not null + dispatch_queue_set_specific(loggingQueue, GlobalLoggingQueueIdentityKey, nonNullValue, NULL); + + queueSemaphore = dispatch_semaphore_create(LOG_MAX_QUEUE_SIZE); + + // Figure out how many processors are available. + // This may be used later for an optimization on uniprocessor machines. + + host_basic_info_data_t hostInfo; + mach_msg_type_number_t infoCount; + + infoCount = HOST_BASIC_INFO_COUNT; + host_info(mach_host_self(), HOST_BASIC_INFO, (host_info_t)&hostInfo, &infoCount); + + unsigned int result = (unsigned int)(hostInfo.max_cpus); + unsigned int one = (unsigned int)(1); + + numProcessors = MAX(result, one); + + NSLogDebug(@"DDLog: numProcessors = %u", numProcessors); + + + #if TARGET_OS_IPHONE + NSString *notificationName = @"UIApplicationWillTerminateNotification"; + #else + NSString *notificationName = @"NSApplicationWillTerminateNotification"; + #endif + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillTerminate:) + name:notificationName + object:nil]; + } +} + +/** + * Provides access to the logging queue. +**/ ++ (dispatch_queue_t)loggingQueue +{ + return loggingQueue; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (void)applicationWillTerminate:(NSNotification *)notification +{ + [self flushLog]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Logger Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (void)addLogger:(id )logger +{ + if (logger == nil) return; + + dispatch_async(loggingQueue, ^{ @autoreleasepool { + + [self lt_addLogger:logger]; + }}); +} + ++ (void)removeLogger:(id )logger +{ + if (logger == nil) return; + + dispatch_async(loggingQueue, ^{ @autoreleasepool { + + [self lt_removeLogger:logger]; + }}); +} + ++ (void)removeAllLoggers +{ + dispatch_async(loggingQueue, ^{ @autoreleasepool { + + [self lt_removeAllLoggers]; + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Master Logging +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag +{ + // We have a tricky situation here... + // + // In the common case, when the queueSize is below the maximumQueueSize, + // we want to simply enqueue the logMessage. And we want to do this as fast as possible, + // which means we don't want to block and we don't want to use any locks. + // + // However, if the queueSize gets too big, we want to block. + // But we have very strict requirements as to when we block, and how long we block. + // + // The following example should help illustrate our requirements: + // + // Imagine that the maximum queue size is configured to be 5, + // and that there are already 5 log messages queued. + // Let us call these 5 queued log messages A, B, C, D, and E. (A is next to be executed) + // + // Now if our thread issues a log statement (let us call the log message F), + // it should block before the message is added to the queue. + // Furthermore, it should be unblocked immediately after A has been unqueued. + // + // The requirements are strict in this manner so that we block only as long as necessary, + // and so that blocked threads are unblocked in the order in which they were blocked. + // + // Returning to our previous example, let us assume that log messages A through E are still queued. + // Our aforementioned thread is blocked attempting to queue log message F. + // Now assume we have another separate thread that attempts to issue log message G. + // It should block until log messages A and B have been unqueued. + + + // We are using a counting semaphore provided by GCD. + // The semaphore is initialized with our LOG_MAX_QUEUE_SIZE value. + // Everytime we want to queue a log message we decrement this value. + // If the resulting value is less than zero, + // the semaphore function waits in FIFO order for a signal to occur before returning. + // + // A dispatch semaphore is an efficient implementation of a traditional counting semaphore. + // Dispatch semaphores call down to the kernel only when the calling thread needs to be blocked. + // If the calling semaphore does not need to block, no kernel call is made. + + dispatch_semaphore_wait(queueSemaphore, DISPATCH_TIME_FOREVER); + + // We've now sure we won't overflow the queue. + // It is time to queue our log message. + + dispatch_block_t logBlock = ^{ @autoreleasepool { + + [self lt_log:logMessage]; + }}; + + if (asyncFlag) + dispatch_async(loggingQueue, logBlock); + else + dispatch_sync(loggingQueue, logBlock); +} + ++ (void)log:(BOOL)asynchronous + level:(int)level + flag:(int)flag + context:(int)context + file:(const char *)file + function:(const char *)function + line:(int)line + tag:(id)tag + format:(NSString *)format, ... +{ + va_list args; + if (format) + { + va_start(args, format); + + NSString *logMsg = [[NSString alloc] initWithFormat:format arguments:args]; + DDLogMessage *logMessage = [[DDLogMessage alloc] initWithLogMsg:logMsg + level:level + flag:flag + context:context + file:file + function:function + line:line + tag:tag + options:0]; + + [self queueLogMessage:logMessage asynchronously:asynchronous]; + + va_end(args); + } +} + ++ (void)log:(BOOL)asynchronous + level:(int)level + flag:(int)flag + context:(int)context + file:(const char *)file + function:(const char *)function + line:(int)line + tag:(id)tag + format:(NSString *)format + args:(va_list)args +{ + if (format) + { + NSString *logMsg = [[NSString alloc] initWithFormat:format arguments:args]; + DDLogMessage *logMessage = [[DDLogMessage alloc] initWithLogMsg:logMsg + level:level + flag:flag + context:context + file:file + function:function + line:line + tag:tag + options:0]; + + [self queueLogMessage:logMessage asynchronously:asynchronous]; + } +} + ++ (void)flushLog +{ + dispatch_sync(loggingQueue, ^{ @autoreleasepool { + + [self lt_flush]; + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Registered Dynamic Logging +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (BOOL)isRegisteredClass:(Class)class +{ + SEL getterSel = @selector(ddLogLevel); + SEL setterSel = @selector(ddSetLogLevel:); + +#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + + // Issue #6 (GoogleCode) - Crashes on iOS 4.2.1 and iPhone 4 + // + // Crash caused by class_getClassMethod(2). + // + // "It's a bug with UIAccessibilitySafeCategory__NSObject so it didn't pop up until + // users had VoiceOver enabled [...]. I was able to work around it by searching the + // result of class_copyMethodList() instead of calling class_getClassMethod()" + + BOOL result = NO; + + unsigned int methodCount, i; + Method *methodList = class_copyMethodList(object_getClass(class), &methodCount); + + if (methodList != NULL) + { + BOOL getterFound = NO; + BOOL setterFound = NO; + + for (i = 0; i < methodCount; ++i) + { + SEL currentSel = method_getName(methodList[i]); + + if (currentSel == getterSel) + { + getterFound = YES; + } + else if (currentSel == setterSel) + { + setterFound = YES; + } + + if (getterFound && setterFound) + { + result = YES; + break; + } + } + + free(methodList); + } + + return result; + +#else + + // Issue #24 (GitHub) - Crashing in in ARC+Simulator + // + // The method +[DDLog isRegisteredClass] will crash a project when using it with ARC + Simulator. + // For running in the Simulator, it needs to execute the non-iOS code. + + Method getter = class_getClassMethod(class, getterSel); + Method setter = class_getClassMethod(class, setterSel); + + if ((getter != NULL) && (setter != NULL)) + { + return YES; + } + + return NO; + +#endif +} + ++ (NSArray *)registeredClasses +{ + int numClasses, i; + + // We're going to get the list of all registered classes. + // The Objective-C runtime library automatically registers all the classes defined in your source code. + // + // To do this we use the following method (documented in the Objective-C Runtime Reference): + // + // int objc_getClassList(Class *buffer, int bufferLen) + // + // We can pass (NULL, 0) to obtain the total number of + // registered class definitions without actually retrieving any class definitions. + // This allows us to allocate the minimum amount of memory needed for the application. + + numClasses = objc_getClassList(NULL, 0); + + // The numClasses method now tells us how many classes we have. + // So we can allocate our buffer, and get pointers to all the class definitions. + + Class *classes = (Class *)malloc(sizeof(Class) * numClasses); + + numClasses = objc_getClassList(classes, numClasses); + + // We can now loop through the classes, and test each one to see if it is a DDLogging class. + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:numClasses]; + + for (i = 0; i < numClasses; i++) + { + Class class = classes[i]; + + if ([self isRegisteredClass:class]) + { + [result addObject:class]; + } + } + + free(classes); + + return result; +} + ++ (NSArray *)registeredClassNames +{ + NSArray *registeredClasses = [self registeredClasses]; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:[registeredClasses count]]; + + for (Class class in registeredClasses) + { + [result addObject:NSStringFromClass(class)]; + } + + return result; +} + ++ (int)logLevelForClass:(Class)aClass +{ + if ([self isRegisteredClass:aClass]) + { + return [aClass ddLogLevel]; + } + + return -1; +} + ++ (int)logLevelForClassWithName:(NSString *)aClassName +{ + Class aClass = NSClassFromString(aClassName); + + return [self logLevelForClass:aClass]; +} + ++ (void)setLogLevel:(int)logLevel forClass:(Class)aClass +{ + if ([self isRegisteredClass:aClass]) + { + [aClass ddSetLogLevel:logLevel]; + } +} + ++ (void)setLogLevel:(int)logLevel forClassWithName:(NSString *)aClassName +{ + Class aClass = NSClassFromString(aClassName); + + [self setLogLevel:logLevel forClass:aClass]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Logging Thread +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method should only be run on the logging thread/queue. +**/ ++ (void)lt_addLogger:(id )logger +{ + // Add to loggers array. + // Need to create loggerQueue if loggerNode doesn't provide one. + + dispatch_queue_t loggerQueue = NULL; + + if ([logger respondsToSelector:@selector(loggerQueue)]) + { + // Logger may be providing its own queue + + loggerQueue = [logger loggerQueue]; + } + + if (loggerQueue == nil) + { + // Automatically create queue for the logger. + // Use the logger name as the queue name if possible. + + const char *loggerQueueName = NULL; + if ([logger respondsToSelector:@selector(loggerName)]) + { + loggerQueueName = [[logger loggerName] UTF8String]; + } + + loggerQueue = dispatch_queue_create(loggerQueueName, NULL); + } + + DDLoggerNode *loggerNode = [DDLoggerNode nodeWithLogger:logger loggerQueue:loggerQueue]; + [loggers addObject:loggerNode]; + + if ([logger respondsToSelector:@selector(didAddLogger)]) + { + dispatch_async(loggerNode->loggerQueue, ^{ @autoreleasepool { + + [logger didAddLogger]; + }}); + } +} + +/** + * This method should only be run on the logging thread/queue. +**/ ++ (void)lt_removeLogger:(id )logger +{ + // Find associated loggerNode in list of added loggers + + DDLoggerNode *loggerNode = nil; + + for (DDLoggerNode *node in loggers) + { + if (node->logger == logger) + { + loggerNode = node; + break; + } + } + + if (loggerNode == nil) + { + NSLogDebug(@"DDLog: Request to remove logger which wasn't added"); + return; + } + + // Notify logger + + if ([logger respondsToSelector:@selector(willRemoveLogger)]) + { + dispatch_async(loggerNode->loggerQueue, ^{ @autoreleasepool { + + [logger willRemoveLogger]; + }}); + } + + // Remove from loggers array + + [loggers removeObject:loggerNode]; +} + +/** + * This method should only be run on the logging thread/queue. +**/ ++ (void)lt_removeAllLoggers +{ + // Notify all loggers + + for (DDLoggerNode *loggerNode in loggers) + { + if ([loggerNode->logger respondsToSelector:@selector(willRemoveLogger)]) + { + dispatch_async(loggerNode->loggerQueue, ^{ @autoreleasepool { + + [loggerNode->logger willRemoveLogger]; + }}); + } + } + + // Remove all loggers from array + + [loggers removeAllObjects]; +} + +/** + * This method should only be run on the logging thread/queue. +**/ ++ (void)lt_log:(DDLogMessage *)logMessage +{ + // Execute the given log message on each of our loggers. + + if (numProcessors > 1) + { + // Execute each logger concurrently, each within its own queue. + // All blocks are added to same group. + // After each block has been queued, wait on group. + // + // The waiting ensures that a slow logger doesn't end up with a large queue of pending log messages. + // This would defeat the purpose of the efforts we made earlier to restrict the max queue size. + + for (DDLoggerNode *loggerNode in loggers) + { + dispatch_group_async(loggingGroup, loggerNode->loggerQueue, ^{ @autoreleasepool { + + [loggerNode->logger logMessage:logMessage]; + + }}); + } + + dispatch_group_wait(loggingGroup, DISPATCH_TIME_FOREVER); + } + else + { + // Execute each logger serialy, each within its own queue. + + for (DDLoggerNode *loggerNode in loggers) + { + dispatch_sync(loggerNode->loggerQueue, ^{ @autoreleasepool { + + [loggerNode->logger logMessage:logMessage]; + + }}); + } + } + + // If our queue got too big, there may be blocked threads waiting to add log messages to the queue. + // Since we've now dequeued an item from the log, we may need to unblock the next thread. + + // We are using a counting semaphore provided by GCD. + // The semaphore is initialized with our LOG_MAX_QUEUE_SIZE value. + // When a log message is queued this value is decremented. + // When a log message is dequeued this value is incremented. + // If the value ever drops below zero, + // the queueing thread blocks and waits in FIFO order for us to signal it. + // + // A dispatch semaphore is an efficient implementation of a traditional counting semaphore. + // Dispatch semaphores call down to the kernel only when the calling thread needs to be blocked. + // If the calling semaphore does not need to block, no kernel call is made. + + dispatch_semaphore_signal(queueSemaphore); +} + +/** + * This method should only be run on the background logging thread. +**/ ++ (void)lt_flush +{ + // All log statements issued before the flush method was invoked have now been executed. + // + // Now we need to propogate the flush request to any loggers that implement the flush method. + // This is designed for loggers that buffer IO. + + for (DDLoggerNode *loggerNode in loggers) + { + if ([loggerNode->logger respondsToSelector:@selector(flush)]) + { + dispatch_group_async(loggingGroup, loggerNode->loggerQueue, ^{ @autoreleasepool { + + [loggerNode->logger flush]; + + }}); + } + } + + dispatch_group_wait(loggingGroup, DISPATCH_TIME_FOREVER); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +NSString *DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy) +{ + if (filePath == NULL) return nil; + + char *lastSlash = NULL; + char *lastDot = NULL; + + char *p = (char *)filePath; + + while (*p != '\0') + { + if (*p == '/') + lastSlash = p; + else if (*p == '.') + lastDot = p; + + p++; + } + + char *subStr; + NSUInteger subLen; + + if (lastSlash) + { + if (lastDot) + { + // lastSlash -> lastDot + subStr = lastSlash + 1; + subLen = lastDot - subStr; + } + else + { + // lastSlash -> endOfString + subStr = lastSlash + 1; + subLen = p - subStr; + } + } + else + { + if (lastDot) + { + // startOfString -> lastDot + subStr = (char *)filePath; + subLen = lastDot - subStr; + } + else + { + // startOfString -> endOfString + subStr = (char *)filePath; + subLen = p - subStr; + } + } + + if (copy) + { + return [[NSString alloc] initWithBytes:subStr + length:subLen + encoding:NSUTF8StringEncoding]; + } + else + { + // We can take advantage of the fact that __FILE__ is a string literal. + // Specifically, we don't need to waste time copying the string. + // We can just tell NSString to point to a range within the string literal. + + return [[NSString alloc] initWithBytesNoCopy:subStr + length:subLen + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + } +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLoggerNode + +- (id)initWithLogger:(id )aLogger loggerQueue:(dispatch_queue_t)aLoggerQueue +{ + if ((self = [super init])) + { + logger = aLogger; + + if (aLoggerQueue) { + loggerQueue = aLoggerQueue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(loggerQueue); + #endif + } + } + return self; +} + ++ (DDLoggerNode *)nodeWithLogger:(id )logger loggerQueue:(dispatch_queue_t)loggerQueue +{ + return [[DDLoggerNode alloc] initWithLogger:logger loggerQueue:loggerQueue]; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (loggerQueue) dispatch_release(loggerQueue); + #endif +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLogMessage + +static char *dd_str_copy(const char *str) +{ + if (str == NULL) return NULL; + + size_t length = strlen(str); + char * result = malloc(length + 1); + strncpy(result, str, length); + result[length] = 0; + + return result; +} + +- (id)initWithLogMsg:(NSString *)msg + level:(int)level + flag:(int)flag + context:(int)context + file:(const char *)aFile + function:(const char *)aFunction + line:(int)line + tag:(id)aTag + options:(DDLogMessageOptions)optionsMask +{ + if ((self = [super init])) + { + logMsg = msg; + logLevel = level; + logFlag = flag; + logContext = context; + lineNumber = line; + tag = aTag; + options = optionsMask; + + if (options & DDLogMessageCopyFile) + file = dd_str_copy(aFile); + else + file = (char *)aFile; + + if (options & DDLogMessageCopyFunction) + function = dd_str_copy(aFunction); + else + function = (char *)aFunction; + + timestamp = [[NSDate alloc] init]; + + machThreadID = pthread_mach_thread_np(pthread_self()); + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + // The documentation for dispatch_get_current_queue() states: + // + // > [This method is] "recommended for debugging and logging purposes only"... + // + // Well that's exactly how we're using it here. Literally for logging purposes only. + // However, Apple has decided to deprecate this method anyway. + // However they have not given us an alternate version of dispatch_queue_get_label() that + // automatically uses the current queue, thus dispatch_get_current_queue() is still required. + // + // If dispatch_get_current_queue() disappears, without a dispatch_queue_get_label() alternative, + // Apple will have effectively taken away our ability to properly log the name of executing dispatch queue. + + dispatch_queue_t currentQueue = dispatch_get_current_queue(); + #pragma clang diagnostic pop + + queueLabel = dd_str_copy(dispatch_queue_get_label(currentQueue)); + + threadName = [[NSThread currentThread] name]; + } + return self; +} + +- (NSString *)threadID +{ + return [[NSString alloc] initWithFormat:@"%x", machThreadID]; +} + +- (NSString *)fileName +{ + return DDExtractFileNameWithoutExtension(file, NO); +} + +- (NSString *)methodName +{ + if (function == NULL) + return nil; + else + return [[NSString alloc] initWithUTF8String:function]; +} + +- (void)dealloc +{ + if (file && (options & DDLogMessageCopyFile)) + free(file); + + if (function && (options & DDLogMessageCopyFunction)) + free(function); + + if (queueLabel) + free(queueLabel); +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDAbstractLogger + +- (id)init +{ + if ((self = [super init])) + { + const char *loggerQueueName = NULL; + if ([self respondsToSelector:@selector(loggerName)]) + { + loggerQueueName = [[self loggerName] UTF8String]; + } + + loggerQueue = dispatch_queue_create(loggerQueueName, NULL); + + // We're going to use dispatch_queue_set_specific() to "mark" our loggerQueue. + // Later we can use dispatch_get_specific() to determine if we're executing on our loggerQueue. + // The documentation states: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // > Specifying a pointer to a string constant is not recommended. + // + // So we're going to use the very convenient key of "self", + // which also works when multiple logger classes extend this class, as each will have a different "self" key. + // + // This is used primarily for thread-safety assertions (via the isOnInternalLoggerQueue method below). + + void *key = (__bridge void *)self; + void *nonNullValue = (__bridge void *)self; + + dispatch_queue_set_specific(loggerQueue, key, nonNullValue, NULL); + } + return self; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (loggerQueue) dispatch_release(loggerQueue); + #endif +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + // Override me +} + +- (id )logFormatter +{ + // This method must be thread safe and intuitive. + // Therefore if somebody executes the following code: + // + // [logger setLogFormatter:myFormatter]; + // formatter = [logger logFormatter]; + // + // They would expect formatter to equal myFormatter. + // This functionality must be ensured by the getter and setter method. + // + // The thread safety must not come at a cost to the performance of the logMessage method. + // This method is likely called sporadically, while the logMessage method is called repeatedly. + // This means, the implementation of this method: + // - Must NOT require the logMessage method to acquire a lock. + // - Must NOT require the logMessage method to access an atomic property (also a lock of sorts). + // + // Thread safety is ensured by executing access to the formatter variable on the loggerQueue. + // This is the same queue that the logMessage method operates on. + // + // Note: The last time I benchmarked the performance of direct access vs atomic property access, + // direct access was over twice as fast on the desktop and over 6 times as fast on the iPhone. + // + // Furthermore, consider the following code: + // + // DDLogVerbose(@"log msg 1"); + // DDLogVerbose(@"log msg 2"); + // [logger setFormatter:myFormatter]; + // DDLogVerbose(@"log msg 3"); + // + // Our intuitive requirement means that the new formatter will only apply to the 3rd log message. + // This must remain true even when using asynchronous logging. + // We must keep in mind the various queue's that are in play here: + // + // loggerQueue : Our own private internal queue that the logMessage method runs on. + // Operations are added to this queue from the global loggingQueue. + // + // globalLoggingQueue : The queue that all log messages go through before they arrive in our loggerQueue. + // + // All log statements go through the serial gloabalLoggingQueue before they arrive at our loggerQueue. + // Thus this method also goes through the serial globalLoggingQueue to ensure intuitive operation. + + // IMPORTANT NOTE: + // + // Methods within the DDLogger implementation MUST access the formatter ivar directly. + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block id result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = formatter; + }); + }); + + return result; +} + +- (void)setLogFormatter:(id )logFormatter +{ + // The design of this method is documented extensively in the logFormatter message (above in code). + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_block_t block = ^{ @autoreleasepool { + + if (formatter != logFormatter) + { + if ([formatter respondsToSelector:@selector(willRemoveFromLogger:)]) { + [formatter willRemoveFromLogger:self]; + } + + formatter = logFormatter; + + if ([formatter respondsToSelector:@selector(didAddToLogger:)]) { + [formatter didAddToLogger:self]; + } + } + }}; + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); +} + +- (dispatch_queue_t)loggerQueue +{ + return loggerQueue; +} + +- (NSString *)loggerName +{ + return NSStringFromClass([self class]); +} + +- (BOOL)isOnGlobalLoggingQueue +{ + return (dispatch_get_specific(GlobalLoggingQueueIdentityKey) != NULL); +} + +- (BOOL)isOnInternalLoggerQueue +{ + void *key = (__bridge void *)self; + return (dispatch_get_specific(key) != NULL); +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDTTYLogger.h b/msext/Class/http/CocoaLumberjack/DDTTYLogger.h new file mode 100755 index 0000000..4cbd2e8 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDTTYLogger.h @@ -0,0 +1,167 @@ +#import +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger for Terminal output or Xcode console output, + * depending on where you are running your code. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs it's output to two places: + * + * - Apple System Log (so it shows up in Console.app) + * - StdErr (if stderr is a TTY, so log statements show up in Xcode console) + * + * To duplicate NSLog() functionality you can simply add this logger and an asl logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use only a file logger and a tty logger. +**/ + +@interface DDTTYLogger : DDAbstractLogger +{ + NSCalendar *calendar; + NSUInteger calendarUnitFlags; + + NSString *appName; + char *app; + size_t appLen; + + NSString *processID; + char *pid; + size_t pidLen; + + BOOL colorsEnabled; + NSMutableArray *colorProfilesArray; + NSMutableDictionary *colorProfilesDict; +} + ++ (DDTTYLogger *)sharedInstance; + +/* Inherited from the DDLogger protocol: + * + * Formatters may optionally be added to any logger. + * + * If no formatter is set, the logger simply logs the message as it is given in logMessage, + * or it may use its own built in formatting style. + * + * More information about formatters can be found here: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters + * + * The actual implementation of these methods is inherited from DDAbstractLogger. + +- (id )logFormatter; +- (void)setLogFormatter:(id )formatter; + +*/ + +/** + * Want to use different colors for different log levels? + * Enable this property. + * + * If you run the application via the Terminal (not Xcode), + * the logger will map colors to xterm-256color or xterm-color (if available). + * + * Xcode does NOT natively support colors in the Xcode debugging console. + * You'll need to install the XcodeColors plugin to see colors in the Xcode console. + * https://github.com/robbiehanson/XcodeColors + * + * The default value if NO. +**/ +@property (readwrite, assign) BOOL colorsEnabled; + +/** + * The default color set (foregroundColor, backgroundColor) is: + * + * - LOG_FLAG_ERROR = (red, nil) + * - LOG_FLAG_WARN = (orange, nil) + * + * You can customize the colors however you see fit. + * Please note that you are passing a flag, NOT a level. + * + * GOOD : [ttyLogger setForegroundColor:pink backgroundColor:nil forFlag:LOG_FLAG_INFO]; // <- Good :) + * BAD : [ttyLogger setForegroundColor:pink backgroundColor:nil forFlag:LOG_LEVEL_INFO]; // <- BAD! :( + * + * LOG_FLAG_INFO = 0...00100 + * LOG_LEVEL_INFO = 0...00111 <- Would match LOG_FLAG_INFO and LOG_FLAG_WARN and LOG_FLAG_ERROR + * + * If you run the application within Xcode, then the XcodeColors plugin is required. + * + * If you run the application from a shell, then DDTTYLogger will automatically map the given color to + * the closest available color. (xterm-256color or xterm-color which have 256 and 16 supported colors respectively.) + * + * This method invokes setForegroundColor:backgroundColor:forFlag:context: and passes the default context (0). +**/ +#if TARGET_OS_IPHONE +- (void)setForegroundColor:(UIColor *)txtColor backgroundColor:(UIColor *)bgColor forFlag:(int)mask; +#else +- (void)setForegroundColor:(NSColor *)txtColor backgroundColor:(NSColor *)bgColor forFlag:(int)mask; +#endif + +/** + * Just like setForegroundColor:backgroundColor:flag, but allows you to specify a particular logging context. + * + * A logging context is often used to identify log messages coming from a 3rd party framework, + * although logging context's can be used for many different functions. + * + * Logging context's are explained in further detail here: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomContext +**/ +#if TARGET_OS_IPHONE +- (void)setForegroundColor:(UIColor *)txtColor backgroundColor:(UIColor *)bgColor forFlag:(int)mask context:(int)ctxt; +#else +- (void)setForegroundColor:(NSColor *)txtColor backgroundColor:(NSColor *)bgColor forFlag:(int)mask context:(int)ctxt; +#endif + +/** + * Similar to the methods above, but allows you to map DDLogMessage->tag to a particular color profile. + * For example, you could do something like this: + * + * static NSString *const PurpleTag = @"PurpleTag"; + * + * #define DDLogPurple(frmt, ...) LOG_OBJC_TAG_MACRO(NO, 0, 0, 0, PurpleTag, frmt, ##__VA_ARGS__) + * + * And then in your applicationDidFinishLaunching, or wherever you configure Lumberjack: + * + * #if TARGET_OS_IPHONE + * UIColor *purple = [UIColor colorWithRed:(64/255.0) green:(0/255.0) blue:(128/255.0) alpha:1.0]; + * #else + * NSColor *purple = [NSColor colorWithCalibratedRed:(64/255.0) green:(0/255.0) blue:(128/255.0) alpha:1.0]; + * + * [[DDTTYLogger sharedInstance] setForegroundColor:purple backgroundColor:nil forTag:PurpleTag]; + * [DDLog addLogger:[DDTTYLogger sharedInstance]]; + * + * This would essentially give you a straight NSLog replacement that prints in purple: + * + * DDLogPurple(@"I'm a purple log message!"); +**/ +#if TARGET_OS_IPHONE +- (void)setForegroundColor:(UIColor *)txtColor backgroundColor:(UIColor *)bgColor forTag:(id )tag; +#else +- (void)setForegroundColor:(NSColor *)txtColor backgroundColor:(NSColor *)bgColor forTag:(id )tag; +#endif + +/** + * Clearing color profiles. +**/ +- (void)clearColorsForFlag:(int)mask; +- (void)clearColorsForFlag:(int)mask context:(int)context; +- (void)clearColorsForTag:(id )tag; +- (void)clearColorsForAllFlags; +- (void)clearColorsForAllTags; +- (void)clearAllColors; + +@end diff --git a/msext/Class/http/CocoaLumberjack/DDTTYLogger.m b/msext/Class/http/CocoaLumberjack/DDTTYLogger.m new file mode 100755 index 0000000..2906463 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/DDTTYLogger.m @@ -0,0 +1,1479 @@ +#import "DDTTYLogger.h" + +#import +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// We probably shouldn't be using DDLog() statements within the DDLog implementation. +// But we still want to leave our log statements for any future debugging, +// and to allow other developers to trace the implementation (which is a great learning tool). +// +// So we use primitive logging macros around NSLog. +// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. + +#define LOG_LEVEL 2 + +#define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) + +// Xcode does NOT natively support colors in the Xcode debugging console. +// You'll need to install the XcodeColors plugin to see colors in the Xcode console. +// https://github.com/robbiehanson/XcodeColors +// +// The following is documentation from the XcodeColors project: +// +// +// How to apply color formatting to your log statements: +// +// To set the foreground color: +// Insert the ESCAPE_SEQ into your string, followed by "fg124,12,255;" where r=124, g=12, b=255. +// +// To set the background color: +// Insert the ESCAPE_SEQ into your string, followed by "bg12,24,36;" where r=12, g=24, b=36. +// +// To reset the foreground color (to default value): +// Insert the ESCAPE_SEQ into your string, followed by "fg;" +// +// To reset the background color (to default value): +// Insert the ESCAPE_SEQ into your string, followed by "bg;" +// +// To reset the foreground and background color (to default values) in one operation: +// Insert the ESCAPE_SEQ into your string, followed by ";" + +#define XCODE_COLORS_ESCAPE_SEQ "\033[" + +#define XCODE_COLORS_RESET_FG XCODE_COLORS_ESCAPE_SEQ "fg;" // Clear any foreground color +#define XCODE_COLORS_RESET_BG XCODE_COLORS_ESCAPE_SEQ "bg;" // Clear any background color +#define XCODE_COLORS_RESET XCODE_COLORS_ESCAPE_SEQ ";" // Clear any foreground or background color + +// Some simple defines to make life easier on ourself + +#if TARGET_OS_IPHONE + #define MakeColor(r, g, b) [UIColor colorWithRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0f] +#else + #define MakeColor(r, g, b) [NSColor colorWithCalibratedRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0f] +#endif + +#if TARGET_OS_IPHONE + #define OSColor UIColor +#else + #define OSColor NSColor +#endif + +// If running in a shell, not all RGB colors will be supported. +// In this case we automatically map to the closest available color. +// In order to provide this mapping, we have a hard-coded set of the standard RGB values available in the shell. +// However, not every shell is the same, and Apple likes to think different even when it comes to shell colors. +// +// Map to standard Terminal.app colors (1), or +// map to standard xterm colors (0). + +#define MAP_TO_TERMINAL_APP_COLORS 1 + + +@interface DDTTYLoggerColorProfile : NSObject { +@public + int mask; + int context; + + uint8_t fg_r; + uint8_t fg_g; + uint8_t fg_b; + + uint8_t bg_r; + uint8_t bg_g; + uint8_t bg_b; + + NSUInteger fgCodeIndex; + NSString *fgCodeRaw; + + NSUInteger bgCodeIndex; + NSString *bgCodeRaw; + + char fgCode[24]; + size_t fgCodeLen; + + char bgCode[24]; + size_t bgCodeLen; + + char resetCode[8]; + size_t resetCodeLen; +} + +- (id)initWithForegroundColor:(OSColor *)fgColor backgroundColor:(OSColor *)bgColor flag:(int)mask context:(int)ctxt; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDTTYLogger + +static BOOL isaColorTTY; +static BOOL isaColor256TTY; +static BOOL isaXcodeColorTTY; + +static NSArray *codes_fg = nil; +static NSArray *codes_bg = nil; +static NSArray *colors = nil; + +static DDTTYLogger *sharedInstance; + +/** + * Initializes the colors array, as well as the codes_fg and codes_bg arrays, for 16 color mode. + * + * This method is used when the application is running from within a shell that only supports 16 color mode. + * This method is not invoked if the application is running within Xcode, or via normal UI app launch. +**/ ++ (void)initialize_colors_16 +{ + if (codes_fg || codes_bg || colors) return; + + NSMutableArray *m_codes_fg = [NSMutableArray arrayWithCapacity:16]; + NSMutableArray *m_codes_bg = [NSMutableArray arrayWithCapacity:16]; + NSMutableArray *m_colors = [NSMutableArray arrayWithCapacity:16]; + + // In a standard shell only 16 colors are supported. + // + // More information about ansi escape codes can be found online. + // http://en.wikipedia.org/wiki/ANSI_escape_code + + [m_codes_fg addObject:@"30m"]; // normal - black + [m_codes_fg addObject:@"31m"]; // normal - red + [m_codes_fg addObject:@"32m"]; // normal - green + [m_codes_fg addObject:@"33m"]; // normal - yellow + [m_codes_fg addObject:@"34m"]; // normal - blue + [m_codes_fg addObject:@"35m"]; // normal - magenta + [m_codes_fg addObject:@"36m"]; // normal - cyan + [m_codes_fg addObject:@"37m"]; // normal - gray + [m_codes_fg addObject:@"1;30m"]; // bright - darkgray + [m_codes_fg addObject:@"1;31m"]; // bright - red + [m_codes_fg addObject:@"1;32m"]; // bright - green + [m_codes_fg addObject:@"1;33m"]; // bright - yellow + [m_codes_fg addObject:@"1;34m"]; // bright - blue + [m_codes_fg addObject:@"1;35m"]; // bright - magenta + [m_codes_fg addObject:@"1;36m"]; // bright - cyan + [m_codes_fg addObject:@"1;37m"]; // bright - white + + [m_codes_bg addObject:@"40m"]; // normal - black + [m_codes_bg addObject:@"41m"]; // normal - red + [m_codes_bg addObject:@"42m"]; // normal - green + [m_codes_bg addObject:@"43m"]; // normal - yellow + [m_codes_bg addObject:@"44m"]; // normal - blue + [m_codes_bg addObject:@"45m"]; // normal - magenta + [m_codes_bg addObject:@"46m"]; // normal - cyan + [m_codes_bg addObject:@"47m"]; // normal - gray + [m_codes_bg addObject:@"1;40m"]; // bright - darkgray + [m_codes_bg addObject:@"1;41m"]; // bright - red + [m_codes_bg addObject:@"1;42m"]; // bright - green + [m_codes_bg addObject:@"1;43m"]; // bright - yellow + [m_codes_bg addObject:@"1;44m"]; // bright - blue + [m_codes_bg addObject:@"1;45m"]; // bright - magenta + [m_codes_bg addObject:@"1;46m"]; // bright - cyan + [m_codes_bg addObject:@"1;47m"]; // bright - white + +#if MAP_TO_TERMINAL_APP_COLORS + + // Standard Terminal.app colors: + // + // These are the default colors used by Apple's Terminal.app. + + [m_colors addObject:MakeColor( 0, 0, 0)]; // normal - black + [m_colors addObject:MakeColor(194, 54, 33)]; // normal - red + [m_colors addObject:MakeColor( 37, 188, 36)]; // normal - green + [m_colors addObject:MakeColor(173, 173, 39)]; // normal - yellow + [m_colors addObject:MakeColor( 73, 46, 225)]; // normal - blue + [m_colors addObject:MakeColor(211, 56, 211)]; // normal - magenta + [m_colors addObject:MakeColor( 51, 187, 200)]; // normal - cyan + [m_colors addObject:MakeColor(203, 204, 205)]; // normal - gray + [m_colors addObject:MakeColor(129, 131, 131)]; // bright - darkgray + [m_colors addObject:MakeColor(252, 57, 31)]; // bright - red + [m_colors addObject:MakeColor( 49, 231, 34)]; // bright - green + [m_colors addObject:MakeColor(234, 236, 35)]; // bright - yellow + [m_colors addObject:MakeColor( 88, 51, 255)]; // bright - blue + [m_colors addObject:MakeColor(249, 53, 248)]; // bright - magenta + [m_colors addObject:MakeColor( 20, 240, 240)]; // bright - cyan + [m_colors addObject:MakeColor(233, 235, 235)]; // bright - white + +#else + + // Standard xterm colors: + // + // These are the default colors used by most xterm shells. + + [m_colors addObject:MakeColor( 0, 0, 0)]; // normal - black + [m_colors addObject:MakeColor(205, 0, 0)]; // normal - red + [m_colors addObject:MakeColor( 0, 205, 0)]; // normal - green + [m_colors addObject:MakeColor(205, 205, 0)]; // normal - yellow + [m_colors addObject:MakeColor( 0, 0, 238)]; // normal - blue + [m_colors addObject:MakeColor(205, 0, 205)]; // normal - magenta + [m_colors addObject:MakeColor( 0, 205, 205)]; // normal - cyan + [m_colors addObject:MakeColor(229, 229, 229)]; // normal - gray + [m_colors addObject:MakeColor(127, 127, 127)]; // bright - darkgray + [m_colors addObject:MakeColor(255, 0, 0)]; // bright - red + [m_colors addObject:MakeColor( 0, 255, 0)]; // bright - green + [m_colors addObject:MakeColor(255, 255, 0)]; // bright - yellow + [m_colors addObject:MakeColor( 92, 92, 255)]; // bright - blue + [m_colors addObject:MakeColor(255, 0, 255)]; // bright - magenta + [m_colors addObject:MakeColor( 0, 255, 255)]; // bright - cyan + [m_colors addObject:MakeColor(255, 255, 255)]; // bright - white + +#endif + + codes_fg = [m_codes_fg copy]; + codes_bg = [m_codes_bg copy]; + colors = [m_colors copy]; + + NSAssert([codes_fg count] == [codes_bg count], @"Invalid colors/codes array(s)"); + NSAssert([codes_fg count] == [colors count], @"Invalid colors/codes array(s)"); +} + +/** + * Initializes the colors array, as well as the codes_fg and codes_bg arrays, for 256 color mode. + * + * This method is used when the application is running from within a shell that supports 256 color mode. + * This method is not invoked if the application is running within Xcode, or via normal UI app launch. +**/ ++ (void)initialize_colors_256 +{ + if (codes_fg || codes_bg || colors) return; + + NSMutableArray *m_codes_fg = [NSMutableArray arrayWithCapacity:(256-16)]; + NSMutableArray *m_codes_bg = [NSMutableArray arrayWithCapacity:(256-16)]; + NSMutableArray *m_colors = [NSMutableArray arrayWithCapacity:(256-16)]; + + #if MAP_TO_TERMINAL_APP_COLORS + + // Standard Terminal.app colors: + // + // These are the colors the Terminal.app uses in xterm-256color mode. + // In this mode, the terminal supports 256 different colors, specified by 256 color codes. + // + // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode. + // These are actually configurable, and thus we ignore them for the purposes of mapping, + // as we can't rely on them being constant. They are largely duplicated anyway. + // + // The next 216 color codes are designed to run the spectrum, with several shades of every color. + // While the color codes are standardized, the actual RGB values for each color code is not. + // Apple's Terminal.app uses different RGB values from that of a standard xterm. + // Apple's choices in colors are designed to be a little nicer on the eyes. + // + // The last 24 color codes represent a grayscale. + // + // Unfortunately, unlike the standard xterm color chart, + // Apple's RGB values cannot be calculated using a simple formula (at least not that I know of). + // Also, I don't know of any ways to programmatically query the shell for the RGB values. + // So this big giant color chart had to be made by hand. + // + // More information about ansi escape codes can be found online. + // http://en.wikipedia.org/wiki/ANSI_escape_code + + // Colors + + [m_colors addObject:MakeColor( 47, 49, 49)]; + [m_colors addObject:MakeColor( 60, 42, 144)]; + [m_colors addObject:MakeColor( 66, 44, 183)]; + [m_colors addObject:MakeColor( 73, 46, 222)]; + [m_colors addObject:MakeColor( 81, 50, 253)]; + [m_colors addObject:MakeColor( 88, 51, 255)]; + + [m_colors addObject:MakeColor( 42, 128, 37)]; + [m_colors addObject:MakeColor( 42, 127, 128)]; + [m_colors addObject:MakeColor( 44, 126, 169)]; + [m_colors addObject:MakeColor( 56, 125, 209)]; + [m_colors addObject:MakeColor( 59, 124, 245)]; + [m_colors addObject:MakeColor( 66, 123, 255)]; + + [m_colors addObject:MakeColor( 51, 163, 41)]; + [m_colors addObject:MakeColor( 39, 162, 121)]; + [m_colors addObject:MakeColor( 42, 161, 162)]; + [m_colors addObject:MakeColor( 53, 160, 202)]; + [m_colors addObject:MakeColor( 45, 159, 240)]; + [m_colors addObject:MakeColor( 58, 158, 255)]; + + [m_colors addObject:MakeColor( 31, 196, 37)]; + [m_colors addObject:MakeColor( 48, 196, 115)]; + [m_colors addObject:MakeColor( 39, 195, 155)]; + [m_colors addObject:MakeColor( 49, 195, 195)]; + [m_colors addObject:MakeColor( 32, 194, 235)]; + [m_colors addObject:MakeColor( 53, 193, 255)]; + + [m_colors addObject:MakeColor( 50, 229, 35)]; + [m_colors addObject:MakeColor( 40, 229, 109)]; + [m_colors addObject:MakeColor( 27, 229, 149)]; + [m_colors addObject:MakeColor( 49, 228, 189)]; + [m_colors addObject:MakeColor( 33, 228, 228)]; + [m_colors addObject:MakeColor( 53, 227, 255)]; + + [m_colors addObject:MakeColor( 27, 254, 30)]; + [m_colors addObject:MakeColor( 30, 254, 103)]; + [m_colors addObject:MakeColor( 45, 254, 143)]; + [m_colors addObject:MakeColor( 38, 253, 182)]; + [m_colors addObject:MakeColor( 38, 253, 222)]; + [m_colors addObject:MakeColor( 42, 253, 252)]; + + [m_colors addObject:MakeColor(140, 48, 40)]; + [m_colors addObject:MakeColor(136, 51, 136)]; + [m_colors addObject:MakeColor(135, 52, 177)]; + [m_colors addObject:MakeColor(134, 52, 217)]; + [m_colors addObject:MakeColor(135, 56, 248)]; + [m_colors addObject:MakeColor(134, 53, 255)]; + + [m_colors addObject:MakeColor(125, 125, 38)]; + [m_colors addObject:MakeColor(124, 125, 125)]; + [m_colors addObject:MakeColor(122, 124, 166)]; + [m_colors addObject:MakeColor(123, 124, 207)]; + [m_colors addObject:MakeColor(123, 122, 247)]; + [m_colors addObject:MakeColor(124, 121, 255)]; + + [m_colors addObject:MakeColor(119, 160, 35)]; + [m_colors addObject:MakeColor(117, 160, 120)]; + [m_colors addObject:MakeColor(117, 160, 160)]; + [m_colors addObject:MakeColor(115, 159, 201)]; + [m_colors addObject:MakeColor(116, 158, 240)]; + [m_colors addObject:MakeColor(117, 157, 255)]; + + [m_colors addObject:MakeColor(113, 195, 39)]; + [m_colors addObject:MakeColor(110, 194, 114)]; + [m_colors addObject:MakeColor(111, 194, 154)]; + [m_colors addObject:MakeColor(108, 194, 194)]; + [m_colors addObject:MakeColor(109, 193, 234)]; + [m_colors addObject:MakeColor(108, 192, 255)]; + + [m_colors addObject:MakeColor(105, 228, 30)]; + [m_colors addObject:MakeColor(103, 228, 109)]; + [m_colors addObject:MakeColor(105, 228, 148)]; + [m_colors addObject:MakeColor(100, 227, 188)]; + [m_colors addObject:MakeColor( 99, 227, 227)]; + [m_colors addObject:MakeColor( 99, 226, 253)]; + + [m_colors addObject:MakeColor( 92, 253, 34)]; + [m_colors addObject:MakeColor( 96, 253, 103)]; + [m_colors addObject:MakeColor( 97, 253, 142)]; + [m_colors addObject:MakeColor( 88, 253, 182)]; + [m_colors addObject:MakeColor( 93, 253, 221)]; + [m_colors addObject:MakeColor( 88, 254, 251)]; + + [m_colors addObject:MakeColor(177, 53, 34)]; + [m_colors addObject:MakeColor(174, 54, 131)]; + [m_colors addObject:MakeColor(172, 55, 172)]; + [m_colors addObject:MakeColor(171, 57, 213)]; + [m_colors addObject:MakeColor(170, 55, 249)]; + [m_colors addObject:MakeColor(170, 57, 255)]; + + [m_colors addObject:MakeColor(165, 123, 37)]; + [m_colors addObject:MakeColor(163, 123, 123)]; + [m_colors addObject:MakeColor(162, 123, 164)]; + [m_colors addObject:MakeColor(161, 122, 205)]; + [m_colors addObject:MakeColor(161, 121, 241)]; + [m_colors addObject:MakeColor(161, 121, 255)]; + + [m_colors addObject:MakeColor(158, 159, 33)]; + [m_colors addObject:MakeColor(157, 158, 118)]; + [m_colors addObject:MakeColor(157, 158, 159)]; + [m_colors addObject:MakeColor(155, 157, 199)]; + [m_colors addObject:MakeColor(155, 157, 239)]; + [m_colors addObject:MakeColor(154, 156, 255)]; + + [m_colors addObject:MakeColor(152, 193, 40)]; + [m_colors addObject:MakeColor(151, 193, 113)]; + [m_colors addObject:MakeColor(150, 193, 153)]; + [m_colors addObject:MakeColor(150, 192, 193)]; + [m_colors addObject:MakeColor(148, 192, 232)]; + [m_colors addObject:MakeColor(149, 191, 253)]; + + [m_colors addObject:MakeColor(146, 227, 28)]; + [m_colors addObject:MakeColor(144, 227, 108)]; + [m_colors addObject:MakeColor(144, 227, 147)]; + [m_colors addObject:MakeColor(144, 227, 187)]; + [m_colors addObject:MakeColor(142, 226, 227)]; + [m_colors addObject:MakeColor(142, 225, 252)]; + + [m_colors addObject:MakeColor(138, 253, 36)]; + [m_colors addObject:MakeColor(137, 253, 102)]; + [m_colors addObject:MakeColor(136, 253, 141)]; + [m_colors addObject:MakeColor(138, 254, 181)]; + [m_colors addObject:MakeColor(135, 255, 220)]; + [m_colors addObject:MakeColor(133, 255, 250)]; + + [m_colors addObject:MakeColor(214, 57, 30)]; + [m_colors addObject:MakeColor(211, 59, 126)]; + [m_colors addObject:MakeColor(209, 57, 168)]; + [m_colors addObject:MakeColor(208, 55, 208)]; + [m_colors addObject:MakeColor(207, 58, 247)]; + [m_colors addObject:MakeColor(206, 61, 255)]; + + [m_colors addObject:MakeColor(204, 121, 32)]; + [m_colors addObject:MakeColor(202, 121, 121)]; + [m_colors addObject:MakeColor(201, 121, 161)]; + [m_colors addObject:MakeColor(200, 120, 202)]; + [m_colors addObject:MakeColor(200, 120, 241)]; + [m_colors addObject:MakeColor(198, 119, 255)]; + + [m_colors addObject:MakeColor(198, 157, 37)]; + [m_colors addObject:MakeColor(196, 157, 116)]; + [m_colors addObject:MakeColor(195, 156, 157)]; + [m_colors addObject:MakeColor(195, 156, 197)]; + [m_colors addObject:MakeColor(194, 155, 236)]; + [m_colors addObject:MakeColor(193, 155, 255)]; + + [m_colors addObject:MakeColor(191, 192, 36)]; + [m_colors addObject:MakeColor(190, 191, 112)]; + [m_colors addObject:MakeColor(189, 191, 152)]; + [m_colors addObject:MakeColor(189, 191, 191)]; + [m_colors addObject:MakeColor(188, 190, 230)]; + [m_colors addObject:MakeColor(187, 190, 253)]; + + [m_colors addObject:MakeColor(185, 226, 28)]; + [m_colors addObject:MakeColor(184, 226, 106)]; + [m_colors addObject:MakeColor(183, 225, 146)]; + [m_colors addObject:MakeColor(183, 225, 186)]; + [m_colors addObject:MakeColor(182, 225, 225)]; + [m_colors addObject:MakeColor(181, 224, 252)]; + + [m_colors addObject:MakeColor(178, 255, 35)]; + [m_colors addObject:MakeColor(178, 255, 101)]; + [m_colors addObject:MakeColor(177, 254, 141)]; + [m_colors addObject:MakeColor(176, 254, 180)]; + [m_colors addObject:MakeColor(176, 254, 220)]; + [m_colors addObject:MakeColor(175, 253, 249)]; + + [m_colors addObject:MakeColor(247, 56, 30)]; + [m_colors addObject:MakeColor(245, 57, 122)]; + [m_colors addObject:MakeColor(243, 59, 163)]; + [m_colors addObject:MakeColor(244, 60, 204)]; + [m_colors addObject:MakeColor(242, 59, 241)]; + [m_colors addObject:MakeColor(240, 55, 255)]; + + [m_colors addObject:MakeColor(241, 119, 36)]; + [m_colors addObject:MakeColor(240, 120, 118)]; + [m_colors addObject:MakeColor(238, 119, 158)]; + [m_colors addObject:MakeColor(237, 119, 199)]; + [m_colors addObject:MakeColor(237, 118, 238)]; + [m_colors addObject:MakeColor(236, 118, 255)]; + + [m_colors addObject:MakeColor(235, 154, 36)]; + [m_colors addObject:MakeColor(235, 154, 114)]; + [m_colors addObject:MakeColor(234, 154, 154)]; + [m_colors addObject:MakeColor(232, 154, 194)]; + [m_colors addObject:MakeColor(232, 153, 234)]; + [m_colors addObject:MakeColor(232, 153, 255)]; + + [m_colors addObject:MakeColor(230, 190, 30)]; + [m_colors addObject:MakeColor(229, 189, 110)]; + [m_colors addObject:MakeColor(228, 189, 150)]; + [m_colors addObject:MakeColor(227, 189, 190)]; + [m_colors addObject:MakeColor(227, 189, 229)]; + [m_colors addObject:MakeColor(226, 188, 255)]; + + [m_colors addObject:MakeColor(224, 224, 35)]; + [m_colors addObject:MakeColor(223, 224, 105)]; + [m_colors addObject:MakeColor(222, 224, 144)]; + [m_colors addObject:MakeColor(222, 223, 184)]; + [m_colors addObject:MakeColor(222, 223, 224)]; + [m_colors addObject:MakeColor(220, 223, 253)]; + + [m_colors addObject:MakeColor(217, 253, 28)]; + [m_colors addObject:MakeColor(217, 253, 99)]; + [m_colors addObject:MakeColor(216, 252, 139)]; + [m_colors addObject:MakeColor(216, 252, 179)]; + [m_colors addObject:MakeColor(215, 252, 218)]; + [m_colors addObject:MakeColor(215, 251, 250)]; + + [m_colors addObject:MakeColor(255, 61, 30)]; + [m_colors addObject:MakeColor(255, 60, 118)]; + [m_colors addObject:MakeColor(255, 58, 159)]; + [m_colors addObject:MakeColor(255, 56, 199)]; + [m_colors addObject:MakeColor(255, 55, 238)]; + [m_colors addObject:MakeColor(255, 59, 255)]; + + [m_colors addObject:MakeColor(255, 117, 29)]; + [m_colors addObject:MakeColor(255, 117, 115)]; + [m_colors addObject:MakeColor(255, 117, 155)]; + [m_colors addObject:MakeColor(255, 117, 195)]; + [m_colors addObject:MakeColor(255, 116, 235)]; + [m_colors addObject:MakeColor(254, 116, 255)]; + + [m_colors addObject:MakeColor(255, 152, 27)]; + [m_colors addObject:MakeColor(255, 152, 111)]; + [m_colors addObject:MakeColor(254, 152, 152)]; + [m_colors addObject:MakeColor(255, 152, 192)]; + [m_colors addObject:MakeColor(254, 151, 231)]; + [m_colors addObject:MakeColor(253, 151, 253)]; + + [m_colors addObject:MakeColor(255, 187, 33)]; + [m_colors addObject:MakeColor(253, 187, 107)]; + [m_colors addObject:MakeColor(252, 187, 148)]; + [m_colors addObject:MakeColor(253, 187, 187)]; + [m_colors addObject:MakeColor(254, 187, 227)]; + [m_colors addObject:MakeColor(252, 186, 252)]; + + [m_colors addObject:MakeColor(252, 222, 34)]; + [m_colors addObject:MakeColor(251, 222, 103)]; + [m_colors addObject:MakeColor(251, 222, 143)]; + [m_colors addObject:MakeColor(250, 222, 182)]; + [m_colors addObject:MakeColor(251, 221, 222)]; + [m_colors addObject:MakeColor(252, 221, 252)]; + + [m_colors addObject:MakeColor(251, 252, 15)]; + [m_colors addObject:MakeColor(251, 252, 97)]; + [m_colors addObject:MakeColor(249, 252, 137)]; + [m_colors addObject:MakeColor(247, 252, 177)]; + [m_colors addObject:MakeColor(247, 253, 217)]; + [m_colors addObject:MakeColor(254, 255, 255)]; + + // Grayscale + + [m_colors addObject:MakeColor( 52, 53, 53)]; + [m_colors addObject:MakeColor( 57, 58, 59)]; + [m_colors addObject:MakeColor( 66, 67, 67)]; + [m_colors addObject:MakeColor( 75, 76, 76)]; + [m_colors addObject:MakeColor( 83, 85, 85)]; + [m_colors addObject:MakeColor( 92, 93, 94)]; + + [m_colors addObject:MakeColor(101, 102, 102)]; + [m_colors addObject:MakeColor(109, 111, 111)]; + [m_colors addObject:MakeColor(118, 119, 119)]; + [m_colors addObject:MakeColor(126, 127, 128)]; + [m_colors addObject:MakeColor(134, 136, 136)]; + [m_colors addObject:MakeColor(143, 144, 145)]; + + [m_colors addObject:MakeColor(151, 152, 153)]; + [m_colors addObject:MakeColor(159, 161, 161)]; + [m_colors addObject:MakeColor(167, 169, 169)]; + [m_colors addObject:MakeColor(176, 177, 177)]; + [m_colors addObject:MakeColor(184, 185, 186)]; + [m_colors addObject:MakeColor(192, 193, 194)]; + + [m_colors addObject:MakeColor(200, 201, 202)]; + [m_colors addObject:MakeColor(208, 209, 210)]; + [m_colors addObject:MakeColor(216, 218, 218)]; + [m_colors addObject:MakeColor(224, 226, 226)]; + [m_colors addObject:MakeColor(232, 234, 234)]; + [m_colors addObject:MakeColor(240, 242, 242)]; + + // Color codes + + int index = 16; + + while (index < 256) + { + [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; + [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; + + index++; + } + + #else + + // Standard xterm colors: + // + // These are the colors xterm shells use in xterm-256color mode. + // In this mode, the shell supports 256 different colors, specified by 256 color codes. + // + // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode. + // These are generally configurable, and thus we ignore them for the purposes of mapping, + // as we can't rely on them being constant. They are largely duplicated anyway. + // + // The next 216 color codes are designed to run the spectrum, with several shades of every color. + // The last 24 color codes represent a grayscale. + // + // While the color codes are standardized, the actual RGB values for each color code is not. + // However most standard xterms follow a well known color chart, + // which can easily be calculated using the simple formula below. + // + // More information about ansi escape codes can be found online. + // http://en.wikipedia.org/wiki/ANSI_escape_code + + int index = 16; + + int r; // red + int g; // green + int b; // blue + + int ri; // r increment + int gi; // g increment + int bi; // b increment + + // Calculate xterm colors (using standard algorithm) + + int r = 0; + int g = 0; + int b = 0; + + for (ri = 0; ri < 6; ri++) + { + r = (ri == 0) ? 0 : 95 + (40 * (ri - 1)); + + for (gi = 0; gi < 6; gi++) + { + g = (gi == 0) ? 0 : 95 + (40 * (gi - 1)); + + for (bi = 0; bi < 6; bi++) + { + b = (bi == 0) ? 0 : 95 + (40 * (bi - 1)); + + [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; + [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; + [m_colors addObject:MakeColor(r, g, b)]; + + index++; + } + } + } + + // Calculate xterm grayscale (using standard algorithm) + + r = 8; + g = 8; + b = 8; + + while (index < 256) + { + [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; + [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; + [m_colors addObject:MakeColor(r, g, b)]; + + r += 10; + g += 10; + b += 10; + + index++; + } + + #endif + + codes_fg = [m_codes_fg copy]; + codes_bg = [m_codes_bg copy]; + colors = [m_colors copy]; + + NSAssert([codes_fg count] == [codes_bg count], @"Invalid colors/codes array(s)"); + NSAssert([codes_fg count] == [colors count], @"Invalid colors/codes array(s)"); +} + ++ (void)getRed:(CGFloat *)rPtr green:(CGFloat *)gPtr blue:(CGFloat *)bPtr fromColor:(OSColor *)color +{ + #if TARGET_OS_IPHONE + + // iOS + + if ([color respondsToSelector:@selector(getRed:green:blue:alpha:)]) + { + [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; + } + else + { + // The method getRed:green:blue:alpha: was only available starting iOS 5. + // So in iOS 4 and earlier, we have to jump through hoops. + + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + + unsigned char pixel[4]; + CGContextRef context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, rgbColorSpace, kCGImageAlphaNoneSkipLast); + + CGContextSetFillColorWithColor(context, [color CGColor]); + CGContextFillRect(context, CGRectMake(0, 0, 1, 1)); + + if (rPtr) { *rPtr = pixel[0] / 255.0f; } + if (gPtr) { *gPtr = pixel[1] / 255.0f; } + if (bPtr) { *bPtr = pixel[2] / 255.0f; } + + CGContextRelease(context); + CGColorSpaceRelease(rgbColorSpace); + } + + #else + + // Mac OS X + + [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; + + #endif +} + +/** + * Maps the given color to the closest available color supported by the shell. + * The shell may support 256 colors, or only 16. + * + * This method loops through the known supported color set, and calculates the closest color. + * The array index of that color, within the colors array, is then returned. + * This array index may also be used as the index within the codes_fg and codes_bg arrays. +**/ ++ (NSUInteger)codeIndexForColor:(OSColor *)inColor +{ + CGFloat inR, inG, inB; + [self getRed:&inR green:&inG blue:&inB fromColor:inColor]; + + NSUInteger bestIndex = 0; + CGFloat lowestDistance = 100.0f; + + NSUInteger i = 0; + for (OSColor *color in colors) + { + // Calculate Euclidean distance (lower value means closer to given color) + + CGFloat r, g, b; + [self getRed:&r green:&g blue:&b fromColor:color]; + + #if CGFLOAT_IS_DOUBLE + CGFloat distance = sqrt(pow(r-inR, 2.0) + pow(g-inG, 2.0) + pow(b-inB, 2.0)); + #else + CGFloat distance = sqrtf(powf(r-inR, 2.0f) + powf(g-inG, 2.0f) + powf(b-inB, 2.0f)); + #endif + + NSLogVerbose(@"DDTTYLogger: %3lu : %.3f,%.3f,%.3f & %.3f,%.3f,%.3f = %.6f", + (unsigned long)i, inR, inG, inB, r, g, b, distance); + + if (distance < lowestDistance) + { + bestIndex = i; + lowestDistance = distance; + + NSLogVerbose(@"DDTTYLogger: New best index = %lu", (unsigned long)bestIndex); + } + + i++; + } + + return bestIndex; +} + +/** + * The runtime sends initialize to each class in a program exactly one time just before the class, + * or any class that inherits from it, is sent its first message from within the program. (Thus the + * method may never be invoked if the class is not used.) The runtime sends the initialize message to + * classes in a thread-safe manner. Superclasses receive this message before their subclasses. + * + * This method may also be called directly (assumably by accident), hence the safety mechanism. +**/ ++ (void)initialize +{ + static BOOL initialized = NO; + if (!initialized) + { + initialized = YES; + + char *term = getenv("TERM"); + if (term) + { + if (strcasestr(term, "color") != NULL) + { + isaColorTTY = YES; + isaColor256TTY = (strcasestr(term, "256") != NULL); + + if (isaColor256TTY) + [self initialize_colors_256]; + else + [self initialize_colors_16]; + } + } + else + { + // Xcode does NOT natively support colors in the Xcode debugging console. + // You'll need to install the XcodeColors plugin to see colors in the Xcode console. + // + // PS - Please read the header file before diving into the source code. + + char *xcode_colors = getenv("XcodeColors"); + if (xcode_colors && (strcmp(xcode_colors, "YES") == 0)) + { + isaXcodeColorTTY = YES; + } + } + + NSLogInfo(@"DDTTYLogger: isaColorTTY = %@", (isaColorTTY ? @"YES" : @"NO")); + NSLogInfo(@"DDTTYLogger: isaColor256TTY: %@", (isaColor256TTY ? @"YES" : @"NO")); + NSLogInfo(@"DDTTYLogger: isaXcodeColorTTY: %@", (isaXcodeColorTTY ? @"YES" : @"NO")); + + sharedInstance = [[DDTTYLogger alloc] init]; + } +} + ++ (DDTTYLogger *)sharedInstance +{ + return sharedInstance; +} + +- (id)init +{ + if (sharedInstance != nil) + { + return nil; + } + + if ((self = [super init])) + { + calendar = [NSCalendar autoupdatingCurrentCalendar]; + + calendarUnitFlags = 0; + calendarUnitFlags |= NSYearCalendarUnit; + calendarUnitFlags |= NSMonthCalendarUnit; + calendarUnitFlags |= NSDayCalendarUnit; + calendarUnitFlags |= NSHourCalendarUnit; + calendarUnitFlags |= NSMinuteCalendarUnit; + calendarUnitFlags |= NSSecondCalendarUnit; + + // Initialze 'app' variable (char *) + + appName = [[NSProcessInfo processInfo] processName]; + + appLen = [appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + app = (char *)malloc(appLen + 1); + + [appName getCString:app maxLength:(appLen+1) encoding:NSUTF8StringEncoding]; + + // Initialize 'pid' variable (char *) + + processID = [NSString stringWithFormat:@"%i", (int)getpid()]; + + pidLen = [processID lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + pid = (char *)malloc(pidLen + 1); + + [processID getCString:pid maxLength:(pidLen+1) encoding:NSUTF8StringEncoding]; + + // Initialize color stuff + + colorsEnabled = NO; + colorProfilesArray = [[NSMutableArray alloc] initWithCapacity:8]; + colorProfilesDict = [[NSMutableDictionary alloc] initWithCapacity:8]; + } + return self; +} + +- (void)loadDefaultColorProfiles +{ + [self setForegroundColor:MakeColor(214, 57, 30) backgroundColor:nil forFlag:LOG_FLAG_ERROR]; + [self setForegroundColor:MakeColor(204, 121, 32) backgroundColor:nil forFlag:LOG_FLAG_WARN]; +} + +- (BOOL)colorsEnabled +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = colorsEnabled; + }); + }); + + return result; +} + +- (void)setColorsEnabled:(BOOL)newColorsEnabled +{ + dispatch_block_t block = ^{ @autoreleasepool { + + colorsEnabled = newColorsEnabled; + + if ([colorProfilesArray count] == 0) { + [self loadDefaultColorProfiles]; + } + }}; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); +} + +- (void)setForegroundColor:(OSColor *)txtColor backgroundColor:(OSColor *)bgColor forFlag:(int)mask +{ + [self setForegroundColor:txtColor backgroundColor:bgColor forFlag:mask context:0]; +} + +- (void)setForegroundColor:(OSColor *)txtColor backgroundColor:(OSColor *)bgColor forFlag:(int)mask context:(int)ctxt +{ + dispatch_block_t block = ^{ @autoreleasepool { + + DDTTYLoggerColorProfile *newColorProfile = + [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor + backgroundColor:bgColor + flag:mask + context:ctxt]; + + NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile); + + NSUInteger i = 0; + for (DDTTYLoggerColorProfile *colorProfile in colorProfilesArray) + { + if ((colorProfile->mask == mask) && (colorProfile->context == ctxt)) + { + break; + } + + i++; + } + + if (i < [colorProfilesArray count]) + [colorProfilesArray replaceObjectAtIndex:i withObject:newColorProfile]; + else + [colorProfilesArray addObject:newColorProfile]; + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)setForegroundColor:(OSColor *)txtColor backgroundColor:(OSColor *)bgColor forTag:(id )tag +{ + NSAssert([(id )tag conformsToProtocol:@protocol(NSCopying)], @"Invalid tag"); + + dispatch_block_t block = ^{ @autoreleasepool { + + DDTTYLoggerColorProfile *newColorProfile = + [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor + backgroundColor:bgColor + flag:0 + context:0]; + + NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile); + + [colorProfilesDict setObject:newColorProfile forKey:tag]; + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)clearColorsForFlag:(int)mask +{ + [self clearColorsForFlag:mask context:0]; +} + +- (void)clearColorsForFlag:(int)mask context:(int)context +{ + dispatch_block_t block = ^{ @autoreleasepool { + + NSUInteger i = 0; + for (DDTTYLoggerColorProfile *colorProfile in colorProfilesArray) + { + if ((colorProfile->mask == mask) && (colorProfile->context == context)) + { + break; + } + + i++; + } + + if (i < [colorProfilesArray count]) + { + [colorProfilesArray removeObjectAtIndex:i]; + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)clearColorsForTag:(id )tag +{ + NSAssert([(id )tag conformsToProtocol:@protocol(NSCopying)], @"Invalid tag"); + + dispatch_block_t block = ^{ @autoreleasepool { + + [colorProfilesDict removeObjectForKey:tag]; + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)clearColorsForAllFlags +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [colorProfilesArray removeAllObjects]; + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)clearColorsForAllTags +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [colorProfilesDict removeAllObjects]; + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)clearAllColors +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [colorProfilesArray removeAllObjects]; + [colorProfilesDict removeAllObjects]; + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + NSString *logMsg = logMessage->logMsg; + BOOL isFormatted = NO; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + isFormatted = logMsg != logMessage->logMsg; + } + + if (logMsg) + { + // Search for a color profile associated with the log message + + DDTTYLoggerColorProfile *colorProfile = nil; + + if (colorsEnabled) + { + if (logMessage->tag) + { + colorProfile = [colorProfilesDict objectForKey:logMessage->tag]; + } + if (colorProfile == nil) + { + for (DDTTYLoggerColorProfile *cp in colorProfilesArray) + { + if ((logMessage->logFlag & cp->mask) && (logMessage->logContext == cp->context)) + { + colorProfile = cp; + break; + } + } + } + } + + // Convert log message to C string. + // + // We use the stack instead of the heap for speed if possible. + // But we're extra cautious to avoid a stack overflow. + + NSUInteger msgLen = [logMsg lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + const BOOL useStack = msgLen < (1024 * 4); + + char msgStack[useStack ? (msgLen + 1) : 1]; // Analyzer doesn't like zero-size array, hence the 1 + char *msg = useStack ? msgStack : (char *)malloc(msgLen + 1); + + [logMsg getCString:msg maxLength:(msgLen + 1) encoding:NSUTF8StringEncoding]; + + // Write the log message to STDERR + + if (isFormatted) + { + // The log message has already been formatted. + + struct iovec v[5]; + + if (colorProfile) + { + v[0].iov_base = colorProfile->fgCode; + v[0].iov_len = colorProfile->fgCodeLen; + + v[1].iov_base = colorProfile->bgCode; + v[1].iov_len = colorProfile->bgCodeLen; + + v[4].iov_base = colorProfile->resetCode; + v[4].iov_len = colorProfile->resetCodeLen; + } + else + { + v[0].iov_base = ""; + v[0].iov_len = 0; + + v[1].iov_base = ""; + v[1].iov_len = 0; + + v[4].iov_base = ""; + v[4].iov_len = 0; + } + + v[2].iov_base = (char *)msg; + v[2].iov_len = msgLen; + + v[3].iov_base = "\n"; + v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1; + + writev(STDERR_FILENO, v, 5); + } + else + { + // The log message is unformatted, so apply standard NSLog style formatting. + + int len; + + // Calculate timestamp. + // The technique below is faster than using NSDateFormatter. + + NSDateComponents *components = [calendar components:calendarUnitFlags fromDate:logMessage->timestamp]; + + NSTimeInterval epoch = [logMessage->timestamp timeIntervalSinceReferenceDate]; + int milliseconds = (int)((epoch - floor(epoch)) * 1000); + + char ts[24]; + len = snprintf(ts, 24, "%04ld-%02ld-%02ld %02ld:%02ld:%02ld:%03d", // yyyy-MM-dd HH:mm:ss:SSS + (long)components.year, + (long)components.month, + (long)components.day, + (long)components.hour, + (long)components.minute, + (long)components.second, milliseconds); + + size_t tsLen = MIN(24-1, len); + + // Calculate thread ID + // + // How many characters do we need for the thread id? + // logMessage->machThreadID is of type mach_port_t, which is an unsigned int. + // + // 1 hex char = 4 bits + // 8 hex chars for 32 bit, plus ending '\0' = 9 + + char tid[9]; + len = snprintf(tid, 9, "%x", logMessage->machThreadID); + + size_t tidLen = MIN(9-1, len); + + // Here is our format: "%s %s[%i:%s] %s", timestamp, appName, processID, threadID, logMsg + + struct iovec v[13]; + + if (colorProfile) + { + v[0].iov_base = colorProfile->fgCode; + v[0].iov_len = colorProfile->fgCodeLen; + + v[1].iov_base = colorProfile->bgCode; + v[1].iov_len = colorProfile->bgCodeLen; + + v[12].iov_base = colorProfile->resetCode; + v[12].iov_len = colorProfile->resetCodeLen; + } + else + { + v[0].iov_base = ""; + v[0].iov_len = 0; + + v[1].iov_base = ""; + v[1].iov_len = 0; + + v[12].iov_base = ""; + v[12].iov_len = 0; + } + + v[2].iov_base = ts; + v[2].iov_len = tsLen; + + v[3].iov_base = " "; + v[3].iov_len = 1; + + v[4].iov_base = app; + v[4].iov_len = appLen; + + v[5].iov_base = "["; + v[5].iov_len = 1; + + v[6].iov_base = pid; + v[6].iov_len = pidLen; + + v[7].iov_base = ":"; + v[7].iov_len = 1; + + v[8].iov_base = tid; + v[8].iov_len = MIN((size_t)8, tidLen); // snprintf doesn't return what you might think + + v[9].iov_base = "] "; + v[9].iov_len = 2; + + v[10].iov_base = (char *)msg; + v[10].iov_len = msgLen; + + v[11].iov_base = "\n"; + v[11].iov_len = (msg[msgLen] == '\n') ? 0 : 1; + + writev(STDERR_FILENO, v, 13); + } + + if (!useStack) { + free(msg); + } + } +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.ttyLogger"; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDTTYLoggerColorProfile + +- (id)initWithForegroundColor:(OSColor *)fgColor backgroundColor:(OSColor *)bgColor flag:(int)aMask context:(int)ctxt +{ + if ((self = [super init])) + { + mask = aMask; + context = ctxt; + + CGFloat r, g, b; + + if (fgColor) + { + [DDTTYLogger getRed:&r green:&g blue:&b fromColor:fgColor]; + + fg_r = (uint8_t)(r * 255.0f); + fg_g = (uint8_t)(g * 255.0f); + fg_b = (uint8_t)(b * 255.0f); + } + if (bgColor) + { + [DDTTYLogger getRed:&r green:&g blue:&b fromColor:bgColor]; + + bg_r = (uint8_t)(r * 255.0f); + bg_g = (uint8_t)(g * 255.0f); + bg_b = (uint8_t)(b * 255.0f); + } + + if (fgColor && isaColorTTY) + { + // Map foreground color to closest available shell color + + fgCodeIndex = [DDTTYLogger codeIndexForColor:fgColor]; + fgCodeRaw = [codes_fg objectAtIndex:fgCodeIndex]; + + NSString *escapeSeq = @"\033["; + + NSUInteger len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSUInteger len2 = [fgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + [escapeSeq getCString:(fgCode) maxLength:(len1+1) encoding:NSUTF8StringEncoding]; + [fgCodeRaw getCString:(fgCode+len1) maxLength:(len2+1) encoding:NSUTF8StringEncoding]; + + fgCodeLen = len1+len2; + } + else if (fgColor && isaXcodeColorTTY) + { + // Convert foreground color to color code sequence + + const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ; + + int result = snprintf(fgCode, 24, "%sfg%u,%u,%u;", escapeSeq, fg_r, fg_g, fg_b); + fgCodeLen = MIN(result, (24-1)); + } + else + { + // No foreground color or no color support + + fgCode[0] = '\0'; + fgCodeLen = 0; + } + + if (bgColor && isaColorTTY) + { + // Map background color to closest available shell color + + bgCodeIndex = [DDTTYLogger codeIndexForColor:bgColor]; + bgCodeRaw = [codes_bg objectAtIndex:bgCodeIndex]; + + NSString *escapeSeq = @"\033["; + + NSUInteger len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSUInteger len2 = [bgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + [escapeSeq getCString:(bgCode) maxLength:(len1+1) encoding:NSUTF8StringEncoding]; + [bgCodeRaw getCString:(bgCode+len1) maxLength:(len2+1) encoding:NSUTF8StringEncoding]; + + bgCodeLen = len1+len2; + } + else if (bgColor && isaXcodeColorTTY) + { + // Convert background color to color code sequence + + const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ; + + int result = snprintf(bgCode, 24, "%sbg%u,%u,%u;", escapeSeq, bg_r, bg_g, bg_b); + bgCodeLen = MIN(result, (24-1)); + } + else + { + // No background color or no color support + + bgCode[0] = '\0'; + bgCodeLen = 0; + } + + if (isaColorTTY) + { + resetCodeLen = snprintf(resetCode, 8, "\033[0m"); + } + else if (isaXcodeColorTTY) + { + resetCodeLen = snprintf(resetCode, 8, XCODE_COLORS_RESET); + } + else + { + resetCode[0] = '\0'; + resetCodeLen = 0; + } + } + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat: + @"", + self, mask, context, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b, fgCodeRaw, bgCodeRaw]; +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/Extensions/ContextFilterLogFormatter.h b/msext/Class/http/CocoaLumberjack/Extensions/ContextFilterLogFormatter.h new file mode 100755 index 0000000..dffc865 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/Extensions/ContextFilterLogFormatter.h @@ -0,0 +1,65 @@ +#import +#import "DDLog.h" + +@class ContextFilterLogFormatter; + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" page. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a log formatter that filters log statements from a logging context not on the whitelist. + * + * A log formatter can be added to any logger to format and/or filter its output. + * You can learn more about log formatters here: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters + * + * You can learn more about logging context's here: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomContext + * + * But here's a quick overview / refresher: + * + * Every log statement has a logging context. + * These come from the underlying logging macros defined in DDLog.h. + * The default logging context is zero. + * You can define multiple logging context's for use in your application. + * For example, logically separate parts of your app each have a different logging context. + * Also 3rd party frameworks that make use of Lumberjack generally use their own dedicated logging context. +**/ +@interface ContextWhitelistFilterLogFormatter : NSObject + +- (id)init; + +- (void)addToWhitelist:(int)loggingContext; +- (void)removeFromWhitelist:(int)loggingContext; + +- (NSArray *)whitelist; + +- (BOOL)isOnWhitelist:(int)loggingContext; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This class provides a log formatter that filters log statements from a logging context on the blacklist. +**/ +@interface ContextBlacklistFilterLogFormatter : NSObject + +- (id)init; + +- (void)addToBlacklist:(int)loggingContext; +- (void)removeFromBlacklist:(int)loggingContext; + +- (NSArray *)blacklist; + +- (BOOL)isOnBlacklist:(int)loggingContext; + +@end diff --git a/msext/Class/http/CocoaLumberjack/Extensions/ContextFilterLogFormatter.m b/msext/Class/http/CocoaLumberjack/Extensions/ContextFilterLogFormatter.m new file mode 100755 index 0000000..9c024ac --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/Extensions/ContextFilterLogFormatter.m @@ -0,0 +1,191 @@ +#import "ContextFilterLogFormatter.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface LoggingContextSet : NSObject + +- (void)addToSet:(int)loggingContext; +- (void)removeFromSet:(int)loggingContext; + +- (NSArray *)currentSet; + +- (BOOL)isInSet:(int)loggingContext; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation ContextWhitelistFilterLogFormatter +{ + LoggingContextSet *contextSet; +} + +- (id)init +{ + if ((self = [super init])) + { + contextSet = [[LoggingContextSet alloc] init]; + } + return self; +} + + +- (void)addToWhitelist:(int)loggingContext +{ + [contextSet addToSet:loggingContext]; +} + +- (void)removeFromWhitelist:(int)loggingContext +{ + [contextSet removeFromSet:loggingContext]; +} + +- (NSArray *)whitelist +{ + return [contextSet currentSet]; +} + +- (BOOL)isOnWhitelist:(int)loggingContext +{ + return [contextSet isInSet:loggingContext]; +} + +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage +{ + if ([self isOnWhitelist:logMessage->logContext]) + return logMessage->logMsg; + else + return nil; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation ContextBlacklistFilterLogFormatter +{ + LoggingContextSet *contextSet; +} + +- (id)init +{ + if ((self = [super init])) + { + contextSet = [[LoggingContextSet alloc] init]; + } + return self; +} + + +- (void)addToBlacklist:(int)loggingContext +{ + [contextSet addToSet:loggingContext]; +} + +- (void)removeFromBlacklist:(int)loggingContext +{ + [contextSet removeFromSet:loggingContext]; +} + +- (NSArray *)blacklist +{ + return [contextSet currentSet]; +} + +- (BOOL)isOnBlacklist:(int)loggingContext +{ + return [contextSet isInSet:loggingContext]; +} + +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage +{ + if ([self isOnBlacklist:logMessage->logContext]) + return nil; + else + return logMessage->logMsg; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation LoggingContextSet +{ + OSSpinLock lock; + NSMutableSet *set; +} + +- (id)init +{ + if ((self = [super init])) + { + set = [[NSMutableSet alloc] init]; + } + return self; +} + + +- (void)addToSet:(int)loggingContext +{ + OSSpinLockLock(&lock); + { + [set addObject:@(loggingContext)]; + } + OSSpinLockUnlock(&lock); +} + +- (void)removeFromSet:(int)loggingContext +{ + OSSpinLockLock(&lock); + { + [set removeObject:@(loggingContext)]; + } + OSSpinLockUnlock(&lock); +} + +- (NSArray *)currentSet +{ + NSArray *result = nil; + + OSSpinLockLock(&lock); + { + result = [set allObjects]; + } + OSSpinLockUnlock(&lock); + + return result; +} + +- (BOOL)isInSet:(int)loggingContext +{ + BOOL result = NO; + + OSSpinLockLock(&lock); + { + result = [set containsObject:@(loggingContext)]; + } + OSSpinLockUnlock(&lock); + + return result; +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/Extensions/DispatchQueueLogFormatter.h b/msext/Class/http/CocoaLumberjack/Extensions/DispatchQueueLogFormatter.h new file mode 100755 index 0000000..9ad8d3f --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/Extensions/DispatchQueueLogFormatter.h @@ -0,0 +1,116 @@ +#import +#import +#import "DDLog.h" + + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" page. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a log formatter that prints the dispatch_queue label instead of the mach_thread_id. + * + * A log formatter can be added to any logger to format and/or filter its output. + * You can learn more about log formatters here: + * https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters + * + * A typical NSLog (or DDTTYLogger) prints detailed info as [:]. + * For example: + * + * 2011-10-17 20:21:45.435 AppName[19928:5207] Your log message here + * + * Where: + * - 19928 = process id + * - 5207 = thread id (mach_thread_id printed in hex) + * + * When using grand central dispatch (GCD), this information is less useful. + * This is because a single serial dispatch queue may be run on any thread from an internally managed thread pool. + * For example: + * + * 2011-10-17 20:32:31.111 AppName[19954:4d07] Message from my_serial_dispatch_queue + * 2011-10-17 20:32:31.112 AppName[19954:5207] Message from my_serial_dispatch_queue + * 2011-10-17 20:32:31.113 AppName[19954:2c55] Message from my_serial_dispatch_queue + * + * This formatter allows you to replace the standard [box:info] with the dispatch_queue name. + * For example: + * + * 2011-10-17 20:32:31.111 AppName[img-scaling] Message from my_serial_dispatch_queue + * 2011-10-17 20:32:31.112 AppName[img-scaling] Message from my_serial_dispatch_queue + * 2011-10-17 20:32:31.113 AppName[img-scaling] Message from my_serial_dispatch_queue + * + * If the dispatch_queue doesn't have a set name, then it falls back to the thread name. + * If the current thread doesn't have a set name, then it falls back to the mach_thread_id in hex (like normal). + * + * Note: If manually creating your own background threads (via NSThread/alloc/init or NSThread/detachNeThread), + * you can use [[NSThread currentThread] setName:(NSString *)]. +**/ +@interface DispatchQueueLogFormatter : NSObject { +@protected + + NSString *dateFormatString; +} + +/** + * Standard init method. + * Configure using properties as desired. +**/ +- (id)init; + +/** + * The minQueueLength restricts the minimum size of the [detail box]. + * If the minQueueLength is set to 0, there is no restriction. + * + * For example, say a dispatch_queue has a label of "diskIO": + * + * If the minQueueLength is 0: [diskIO] + * If the minQueueLength is 4: [diskIO] + * If the minQueueLength is 5: [diskIO] + * If the minQueueLength is 6: [diskIO] + * If the minQueueLength is 7: [diskIO ] + * If the minQueueLength is 8: [diskIO ] + * + * The default minQueueLength is 0 (no minimum, so [detail box] won't be padded). + * + * If you want every [detail box] to have the exact same width, + * set both minQueueLength and maxQueueLength to the same value. +**/ +@property (assign) NSUInteger minQueueLength; + +/** + * The maxQueueLength restricts the number of characters that will be inside the [detail box]. + * If the maxQueueLength is 0, there is no restriction. + * + * For example, say a dispatch_queue has a label of "diskIO": + * + * If the maxQueueLength is 0: [diskIO] + * If the maxQueueLength is 4: [disk] + * If the maxQueueLength is 5: [diskI] + * If the maxQueueLength is 6: [diskIO] + * If the maxQueueLength is 7: [diskIO] + * If the maxQueueLength is 8: [diskIO] + * + * The default maxQueueLength is 0 (no maximum, so [detail box] won't be truncated). + * + * If you want every [detail box] to have the exact same width, + * set both minQueueLength and maxQueueLength to the same value. +**/ +@property (assign) NSUInteger maxQueueLength; + +/** + * Sometimes queue labels have long names like "com.apple.main-queue", + * but you'd prefer something shorter like simply "main". + * + * This method allows you to set such preferred replacements. + * The above example is set by default. + * + * To remove/undo a previous replacement, invoke this method with nil for the 'shortLabel' parameter. +**/ +- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel; +- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel; + +@end diff --git a/msext/Class/http/CocoaLumberjack/Extensions/DispatchQueueLogFormatter.m b/msext/Class/http/CocoaLumberjack/Extensions/DispatchQueueLogFormatter.m new file mode 100755 index 0000000..d348f3d --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/Extensions/DispatchQueueLogFormatter.m @@ -0,0 +1,251 @@ +#import "DispatchQueueLogFormatter.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation DispatchQueueLogFormatter +{ + int32_t atomicLoggerCount; + NSDateFormatter *threadUnsafeDateFormatter; // Use [self stringFromDate] + + OSSpinLock lock; + + NSUInteger _minQueueLength; // _prefix == Only access via atomic property + NSUInteger _maxQueueLength; // _prefix == Only access via atomic property + NSMutableDictionary *_replacements; // _prefix == Only access from within spinlock +} + +- (id)init +{ + if ((self = [super init])) + { + dateFormatString = @"yyyy-MM-dd HH:mm:ss:SSS"; + + atomicLoggerCount = 0; + threadUnsafeDateFormatter = nil; + + _minQueueLength = 0; + _maxQueueLength = 0; + _replacements = [[NSMutableDictionary alloc] init]; + + // Set default replacements: + + [_replacements setObject:@"main" forKey:@"com.apple.main-thread"]; + } + return self; +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize minQueueLength = _minQueueLength; +@synthesize maxQueueLength = _maxQueueLength; + +- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel +{ + NSString *result = nil; + + OSSpinLockLock(&lock); + { + result = [_replacements objectForKey:longLabel]; + } + OSSpinLockUnlock(&lock); + + return result; +} + +- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel +{ + OSSpinLockLock(&lock); + { + if (shortLabel) + [_replacements setObject:shortLabel forKey:longLabel]; + else + [_replacements removeObjectForKey:longLabel]; + } + OSSpinLockUnlock(&lock); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogFormatter +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)stringFromDate:(NSDate *)date +{ + int32_t loggerCount = OSAtomicAdd32(0, &atomicLoggerCount); + + if (loggerCount <= 1) + { + // Single-threaded mode. + + if (threadUnsafeDateFormatter == nil) + { + threadUnsafeDateFormatter = [[NSDateFormatter alloc] init]; + [threadUnsafeDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [threadUnsafeDateFormatter setDateFormat:dateFormatString]; + } + + return [threadUnsafeDateFormatter stringFromDate:date]; + } + else + { + // Multi-threaded mode. + // NSDateFormatter is NOT thread-safe. + + NSString *key = @"DispatchQueueLogFormatter_NSDateFormatter"; + + NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; + NSDateFormatter *dateFormatter = [threadDictionary objectForKey:key]; + + if (dateFormatter == nil) + { + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setDateFormat:dateFormatString]; + + [threadDictionary setObject:dateFormatter forKey:key]; + } + + return [dateFormatter stringFromDate:date]; + } +} + +- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage +{ + // As per the DDLogFormatter contract, this method is always invoked on the same thread/dispatch_queue + + NSUInteger minQueueLength = self.minQueueLength; + NSUInteger maxQueueLength = self.maxQueueLength; + + // Get the name of the queue, thread, or machID (whichever we are to use). + + NSString *queueThreadLabel = nil; + + BOOL useQueueLabel = YES; + BOOL useThreadName = NO; + + if (logMessage->queueLabel) + { + // If you manually create a thread, it's dispatch_queue will have one of the thread names below. + // Since all such threads have the same name, we'd prefer to use the threadName or the machThreadID. + + char *names[] = { "com.apple.root.low-priority", + "com.apple.root.default-priority", + "com.apple.root.high-priority", + "com.apple.root.low-overcommit-priority", + "com.apple.root.default-overcommit-priority", + "com.apple.root.high-overcommit-priority" }; + + int length = sizeof(names) / sizeof(char *); + + int i; + for (i = 0; i < length; i++) + { + if (strcmp(logMessage->queueLabel, names[i]) == 0) + { + useQueueLabel = NO; + useThreadName = [logMessage->threadName length] > 0; + break; + } + } + } + else + { + useQueueLabel = NO; + useThreadName = [logMessage->threadName length] > 0; + } + + if (useQueueLabel || useThreadName) + { + NSString *fullLabel; + NSString *abrvLabel; + + if (useQueueLabel) + fullLabel = @(logMessage->queueLabel); + else + fullLabel = logMessage->threadName; + + OSSpinLockLock(&lock); + { + abrvLabel = [_replacements objectForKey:fullLabel]; + } + OSSpinLockUnlock(&lock); + + if (abrvLabel) + queueThreadLabel = abrvLabel; + else + queueThreadLabel = fullLabel; + } + else + { + queueThreadLabel = [NSString stringWithFormat:@"%x", logMessage->machThreadID]; + } + + // Now use the thread label in the output + + NSUInteger labelLength = [queueThreadLabel length]; + + // labelLength > maxQueueLength : truncate + // labelLength < minQueueLength : padding + // : exact + + if ((maxQueueLength > 0) && (labelLength > maxQueueLength)) + { + // Truncate + + return [queueThreadLabel substringToIndex:maxQueueLength]; + } + else if (labelLength < minQueueLength) + { + // Padding + + NSUInteger numSpaces = minQueueLength - labelLength; + + char spaces[numSpaces + 1]; + memset(spaces, ' ', numSpaces); + spaces[numSpaces] = '\0'; + + return [NSString stringWithFormat:@"%@%s", queueThreadLabel, spaces]; + } + else + { + // Exact + + return queueThreadLabel; + } +} + +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage +{ + NSString *timestamp = [self stringFromDate:(logMessage->timestamp)]; + NSString *queueThreadLabel = [self queueThreadLabelForLogMessage:logMessage]; + + return [NSString stringWithFormat:@"%@ [%@] %@", timestamp, queueThreadLabel, logMessage->logMsg]; +} + +- (void)didAddToLogger:(id )logger +{ + OSAtomicIncrement32(&atomicLoggerCount); +} + +- (void)willRemoveFromLogger:(id )logger +{ + OSAtomicDecrement32(&atomicLoggerCount); +} + +@end diff --git a/msext/Class/http/CocoaLumberjack/Extensions/README.txt b/msext/Class/http/CocoaLumberjack/Extensions/README.txt new file mode 100755 index 0000000..eb20e50 --- /dev/null +++ b/msext/Class/http/CocoaLumberjack/Extensions/README.txt @@ -0,0 +1,7 @@ +This folder contains some sample formatters that may be helpful. + +Feel free to change them, extend them, or use them as the basis for your own custom formatter(s). + +More information about creating your own custom formatters can be found on the wiki: +https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters + diff --git a/msext/Class/http/Core/Categories/DDData.h b/msext/Class/http/Core/Categories/DDData.h new file mode 100755 index 0000000..23f3d1c --- /dev/null +++ b/msext/Class/http/Core/Categories/DDData.h @@ -0,0 +1,14 @@ +#import + +@interface NSData (DDData) + +- (NSData *)md5Digest; + +- (NSData *)sha1Digest; + +- (NSString *)hexStringValue; + +- (NSString *)base64Encoded; +- (NSData *)base64Decoded; + +@end diff --git a/msext/Class/http/Core/Categories/DDData.m b/msext/Class/http/Core/Categories/DDData.m new file mode 100755 index 0000000..82c60df --- /dev/null +++ b/msext/Class/http/Core/Categories/DDData.m @@ -0,0 +1,158 @@ +#import "DDData.h" +#import + + +@implementation NSData (DDData) + +static char encodingTable[64] = { +'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', +'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', +'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', +'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; + +- (NSData *)md5Digest +{ + unsigned char result[CC_MD5_DIGEST_LENGTH]; + + CC_MD5([self bytes], (CC_LONG)[self length], result); + return [NSData dataWithBytes:result length:CC_MD5_DIGEST_LENGTH]; +} + +- (NSData *)sha1Digest +{ + unsigned char result[CC_SHA1_DIGEST_LENGTH]; + + CC_SHA1([self bytes], (CC_LONG)[self length], result); + return [NSData dataWithBytes:result length:CC_SHA1_DIGEST_LENGTH]; +} + +- (NSString *)hexStringValue +{ + NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([self length] * 2)]; + + const unsigned char *dataBuffer = [self bytes]; + int i; + + for (i = 0; i < [self length]; ++i) + { + [stringBuffer appendFormat:@"%02x", (unsigned int)dataBuffer[i]]; + } + + return [stringBuffer copy]; +} + +- (NSString *)base64Encoded +{ + const unsigned char *bytes = [self bytes]; + NSMutableString *result = [NSMutableString stringWithCapacity:[self length]]; + unsigned long ixtext = 0; + unsigned long lentext = [self length]; + long ctremaining = 0; + unsigned char inbuf[3], outbuf[4]; + unsigned short i = 0; + unsigned short charsonline = 0, ctcopy = 0; + unsigned long ix = 0; + + while( YES ) + { + ctremaining = lentext - ixtext; + if( ctremaining <= 0 ) break; + + for( i = 0; i < 3; i++ ) { + ix = ixtext + i; + if( ix < lentext ) inbuf[i] = bytes[ix]; + else inbuf [i] = 0; + } + + outbuf [0] = (inbuf [0] & 0xFC) >> 2; + outbuf [1] = ((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4); + outbuf [2] = ((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6); + outbuf [3] = inbuf [2] & 0x3F; + ctcopy = 4; + + switch( ctremaining ) + { + case 1: + ctcopy = 2; + break; + case 2: + ctcopy = 3; + break; + } + + for( i = 0; i < ctcopy; i++ ) + [result appendFormat:@"%c", encodingTable[outbuf[i]]]; + + for( i = ctcopy; i < 4; i++ ) + [result appendString:@"="]; + + ixtext += 3; + charsonline += 4; + } + + return [NSString stringWithString:result]; +} + +- (NSData *)base64Decoded +{ + const unsigned char *bytes = [self bytes]; + NSMutableData *result = [NSMutableData dataWithCapacity:[self length]]; + + unsigned long ixtext = 0; + unsigned long lentext = [self length]; + unsigned char ch = 0; + unsigned char inbuf[4] = {0, 0, 0, 0}; + unsigned char outbuf[3] = {0, 0, 0}; + short i = 0, ixinbuf = 0; + BOOL flignore = NO; + BOOL flendtext = NO; + + while( YES ) + { + if( ixtext >= lentext ) break; + ch = bytes[ixtext++]; + flignore = NO; + + if( ( ch >= 'A' ) && ( ch <= 'Z' ) ) ch = ch - 'A'; + else if( ( ch >= 'a' ) && ( ch <= 'z' ) ) ch = ch - 'a' + 26; + else if( ( ch >= '0' ) && ( ch <= '9' ) ) ch = ch - '0' + 52; + else if( ch == '+' ) ch = 62; + else if( ch == '=' ) flendtext = YES; + else if( ch == '/' ) ch = 63; + else flignore = YES; + + if( ! flignore ) + { + short ctcharsinbuf = 3; + BOOL flbreak = NO; + + if( flendtext ) + { + if( ! ixinbuf ) break; + if( ( ixinbuf == 1 ) || ( ixinbuf == 2 ) ) ctcharsinbuf = 1; + else ctcharsinbuf = 2; + ixinbuf = 3; + flbreak = YES; + } + + inbuf [ixinbuf++] = ch; + + if( ixinbuf == 4 ) + { + ixinbuf = 0; + outbuf [0] = ( inbuf[0] << 2 ) | ( ( inbuf[1] & 0x30) >> 4 ); + outbuf [1] = ( ( inbuf[1] & 0x0F ) << 4 ) | ( ( inbuf[2] & 0x3C ) >> 2 ); + outbuf [2] = ( ( inbuf[2] & 0x03 ) << 6 ) | ( inbuf[3] & 0x3F ); + + for( i = 0; i < ctcharsinbuf; i++ ) + [result appendBytes:&outbuf[i] length:1]; + } + + if( flbreak ) break; + } + } + + return [NSData dataWithData:result]; +} + +@end diff --git a/msext/Class/http/Core/Categories/DDNumber.h b/msext/Class/http/Core/Categories/DDNumber.h new file mode 100755 index 0000000..2643610 --- /dev/null +++ b/msext/Class/http/Core/Categories/DDNumber.h @@ -0,0 +1,12 @@ +#import + + +@interface NSNumber (DDNumber) + ++ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum; ++ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum; + ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum; ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum; + +@end diff --git a/msext/Class/http/Core/Categories/DDNumber.m b/msext/Class/http/Core/Categories/DDNumber.m new file mode 100755 index 0000000..d384d39 --- /dev/null +++ b/msext/Class/http/Core/Categories/DDNumber.m @@ -0,0 +1,88 @@ +#import "DDNumber.h" + + +@implementation NSNumber (DDNumber) + ++ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, long long = 64 bit + + *pNum = strtoll([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, unsigned long long = 64 bit + + *pNum = strtoull([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On LP64, NSInteger = long = 64 bit + // Otherwise, NSInteger = int = long = 32 bit + + *pNum = strtol([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On LP64, NSUInteger = unsigned long = 64 bit + // Otherwise, NSUInteger = unsigned int = unsigned long = 32 bit + + *pNum = strtoul([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + +@end diff --git a/msext/Class/http/Core/Categories/DDRange.h b/msext/Class/http/Core/Categories/DDRange.h new file mode 100755 index 0000000..e95974a --- /dev/null +++ b/msext/Class/http/Core/Categories/DDRange.h @@ -0,0 +1,56 @@ +/** + * DDRange is the functional equivalent of a 64 bit NSRange. + * The HTTP Server is designed to support very large files. + * On 32 bit architectures (ppc, i386) NSRange uses unsigned 32 bit integers. + * This only supports a range of up to 4 gigabytes. + * By defining our own variant, we can support a range up to 16 exabytes. + * + * All effort is given such that DDRange functions EXACTLY the same as NSRange. +**/ + +#import +#import + +@class NSString; + +typedef struct _DDRange { + UInt64 location; + UInt64 length; +} DDRange; + +typedef DDRange *DDRangePointer; + +NS_INLINE DDRange DDMakeRange(UInt64 loc, UInt64 len) { + DDRange r; + r.location = loc; + r.length = len; + return r; +} + +NS_INLINE UInt64 DDMaxRange(DDRange range) { + return (range.location + range.length); +} + +NS_INLINE BOOL DDLocationInRange(UInt64 loc, DDRange range) { + return (loc - range.location < range.length); +} + +NS_INLINE BOOL DDEqualRanges(DDRange range1, DDRange range2) { + return ((range1.location == range2.location) && (range1.length == range2.length)); +} + +FOUNDATION_EXPORT DDRange DDUnionRange(DDRange range1, DDRange range2); +FOUNDATION_EXPORT DDRange DDIntersectionRange(DDRange range1, DDRange range2); +FOUNDATION_EXPORT NSString *DDStringFromRange(DDRange range); +FOUNDATION_EXPORT DDRange DDRangeFromString(NSString *aString); + +NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2); + +@interface NSValue (NSValueDDRangeExtensions) + ++ (NSValue *)valueWithDDRange:(DDRange)range; +- (DDRange)ddrangeValue; + +- (NSInteger)ddrangeCompare:(NSValue *)ddrangeValue; + +@end diff --git a/msext/Class/http/Core/Categories/DDRange.m b/msext/Class/http/Core/Categories/DDRange.m new file mode 100755 index 0000000..379e7cf --- /dev/null +++ b/msext/Class/http/Core/Categories/DDRange.m @@ -0,0 +1,104 @@ +#import "DDRange.h" +#import "DDNumber.h" + +DDRange DDUnionRange(DDRange range1, DDRange range2) +{ + DDRange result; + + result.location = MIN(range1.location, range2.location); + result.length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - result.location; + + return result; +} + +DDRange DDIntersectionRange(DDRange range1, DDRange range2) +{ + DDRange result; + + if((DDMaxRange(range1) < range2.location) || (DDMaxRange(range2) < range1.location)) + { + return DDMakeRange(0, 0); + } + + result.location = MAX(range1.location, range2.location); + result.length = MIN(DDMaxRange(range1), DDMaxRange(range2)) - result.location; + + return result; +} + +NSString *DDStringFromRange(DDRange range) +{ + return [NSString stringWithFormat:@"{%qu, %qu}", range.location, range.length]; +} + +DDRange DDRangeFromString(NSString *aString) +{ + DDRange result = DDMakeRange(0, 0); + + // NSRange will ignore '-' characters, but not '+' characters + NSCharacterSet *cset = [NSCharacterSet characterSetWithCharactersInString:@"+0123456789"]; + + NSScanner *scanner = [NSScanner scannerWithString:aString]; + [scanner setCharactersToBeSkipped:[cset invertedSet]]; + + NSString *str1 = nil; + NSString *str2 = nil; + + BOOL found1 = [scanner scanCharactersFromSet:cset intoString:&str1]; + BOOL found2 = [scanner scanCharactersFromSet:cset intoString:&str2]; + + if(found1) [NSNumber parseString:str1 intoUInt64:&result.location]; + if(found2) [NSNumber parseString:str2 intoUInt64:&result.length]; + + return result; +} + +NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2) +{ + // Comparison basis: + // Which range would you encouter first if you started at zero, and began walking towards infinity. + // If you encouter both ranges at the same time, which range would end first. + + if(pDDRange1->location < pDDRange2->location) + { + return NSOrderedAscending; + } + if(pDDRange1->location > pDDRange2->location) + { + return NSOrderedDescending; + } + if(pDDRange1->length < pDDRange2->length) + { + return NSOrderedAscending; + } + if(pDDRange1->length > pDDRange2->length) + { + return NSOrderedDescending; + } + + return NSOrderedSame; +} + +@implementation NSValue (NSValueDDRangeExtensions) + ++ (NSValue *)valueWithDDRange:(DDRange)range +{ + return [NSValue valueWithBytes:&range objCType:@encode(DDRange)]; +} + +- (DDRange)ddrangeValue +{ + DDRange result; + [self getValue:&result]; + return result; +} + +- (NSInteger)ddrangeCompare:(NSValue *)other +{ + DDRange r1 = [self ddrangeValue]; + DDRange r2 = [other ddrangeValue]; + + return DDRangeCompare(&r1, &r2); +} + +@end diff --git a/msext/Class/http/Core/HTTPAuthenticationRequest.h b/msext/Class/http/Core/HTTPAuthenticationRequest.h new file mode 100755 index 0000000..4d64236 --- /dev/null +++ b/msext/Class/http/Core/HTTPAuthenticationRequest.h @@ -0,0 +1,45 @@ +#import + +#if TARGET_OS_IPHONE + // Note: You may need to add the CFNetwork Framework to your project + #import +#endif + +@class HTTPMessage; + + +@interface HTTPAuthenticationRequest : NSObject +{ + BOOL isBasic; + BOOL isDigest; + + NSString *base64Credentials; + + NSString *username; + NSString *realm; + NSString *nonce; + NSString *uri; + NSString *qop; + NSString *nc; + NSString *cnonce; + NSString *response; +} +- (id)initWithRequest:(HTTPMessage *)request; + +- (BOOL)isBasic; +- (BOOL)isDigest; + +// Basic +- (NSString *)base64Credentials; + +// Digest +- (NSString *)username; +- (NSString *)realm; +- (NSString *)nonce; +- (NSString *)uri; +- (NSString *)qop; +- (NSString *)nc; +- (NSString *)cnonce; +- (NSString *)response; + +@end diff --git a/msext/Class/http/Core/HTTPAuthenticationRequest.m b/msext/Class/http/Core/HTTPAuthenticationRequest.m new file mode 100755 index 0000000..4d2f51b --- /dev/null +++ b/msext/Class/http/Core/HTTPAuthenticationRequest.m @@ -0,0 +1,195 @@ +#import "HTTPAuthenticationRequest.h" +#import "HTTPMessage.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface HTTPAuthenticationRequest (PrivateAPI) +- (NSString *)quotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header; +- (NSString *)nonquotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header; +@end + + +@implementation HTTPAuthenticationRequest + +- (id)initWithRequest:(HTTPMessage *)request +{ + if ((self = [super init])) + { + NSString *authInfo = [request headerField:@"Authorization"]; + + isBasic = NO; + if ([authInfo length] >= 6) + { + isBasic = [[authInfo substringToIndex:6] caseInsensitiveCompare:@"Basic "] == NSOrderedSame; + } + + isDigest = NO; + if ([authInfo length] >= 7) + { + isDigest = [[authInfo substringToIndex:7] caseInsensitiveCompare:@"Digest "] == NSOrderedSame; + } + + if (isBasic) + { + NSMutableString *temp = [[authInfo substringFromIndex:6] mutableCopy]; + CFStringTrimWhitespace((__bridge CFMutableStringRef)temp); + + base64Credentials = [temp copy]; + } + + if (isDigest) + { + username = [self quotedSubHeaderFieldValue:@"username" fromHeaderFieldValue:authInfo]; + realm = [self quotedSubHeaderFieldValue:@"realm" fromHeaderFieldValue:authInfo]; + nonce = [self quotedSubHeaderFieldValue:@"nonce" fromHeaderFieldValue:authInfo]; + uri = [self quotedSubHeaderFieldValue:@"uri" fromHeaderFieldValue:authInfo]; + + // It appears from RFC 2617 that the qop is to be given unquoted + // Tests show that Firefox performs this way, but Safari does not + // Thus we'll attempt to retrieve the value as nonquoted, but we'll verify it doesn't start with a quote + qop = [self nonquotedSubHeaderFieldValue:@"qop" fromHeaderFieldValue:authInfo]; + if(qop && ([qop characterAtIndex:0] == '"')) + { + qop = [self quotedSubHeaderFieldValue:@"qop" fromHeaderFieldValue:authInfo]; + } + + nc = [self nonquotedSubHeaderFieldValue:@"nc" fromHeaderFieldValue:authInfo]; + cnonce = [self quotedSubHeaderFieldValue:@"cnonce" fromHeaderFieldValue:authInfo]; + response = [self quotedSubHeaderFieldValue:@"response" fromHeaderFieldValue:authInfo]; + } + } + return self; +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accessors: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isBasic { + return isBasic; +} + +- (BOOL)isDigest { + return isDigest; +} + +- (NSString *)base64Credentials { + return base64Credentials; +} + +- (NSString *)username { + return username; +} + +- (NSString *)realm { + return realm; +} + +- (NSString *)nonce { + return nonce; +} + +- (NSString *)uri { + return uri; +} + +- (NSString *)qop { + return qop; +} + +- (NSString *)nc { + return nc; +} + +- (NSString *)cnonce { + return cnonce; +} + +- (NSString *)response { + return response; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Retrieves a "Sub Header Field Value" from a given header field value. + * The sub header field is expected to be quoted. + * + * In the following header field: + * Authorization: Digest username="Mufasa", qop=auth, response="6629fae4939" + * The sub header field titled 'username' is quoted, and this method would return the value @"Mufasa". +**/ +- (NSString *)quotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header +{ + NSRange startRange = [header rangeOfString:[NSString stringWithFormat:@"%@=\"", param]]; + if(startRange.location == NSNotFound) + { + // The param was not found anywhere in the header + return nil; + } + + NSUInteger postStartRangeLocation = startRange.location + startRange.length; + NSUInteger postStartRangeLength = [header length] - postStartRangeLocation; + NSRange postStartRange = NSMakeRange(postStartRangeLocation, postStartRangeLength); + + NSRange endRange = [header rangeOfString:@"\"" options:0 range:postStartRange]; + if(endRange.location == NSNotFound) + { + // The ending double-quote was not found anywhere in the header + return nil; + } + + NSRange subHeaderRange = NSMakeRange(postStartRangeLocation, endRange.location - postStartRangeLocation); + return [header substringWithRange:subHeaderRange]; +} + +/** + * Retrieves a "Sub Header Field Value" from a given header field value. + * The sub header field is expected to not be quoted. + * + * In the following header field: + * Authorization: Digest username="Mufasa", qop=auth, response="6629fae4939" + * The sub header field titled 'qop' is nonquoted, and this method would return the value @"auth". +**/ +- (NSString *)nonquotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header +{ + NSRange startRange = [header rangeOfString:[NSString stringWithFormat:@"%@=", param]]; + if(startRange.location == NSNotFound) + { + // The param was not found anywhere in the header + return nil; + } + + NSUInteger postStartRangeLocation = startRange.location + startRange.length; + NSUInteger postStartRangeLength = [header length] - postStartRangeLocation; + NSRange postStartRange = NSMakeRange(postStartRangeLocation, postStartRangeLength); + + NSRange endRange = [header rangeOfString:@"," options:0 range:postStartRange]; + if(endRange.location == NSNotFound) + { + // The ending comma was not found anywhere in the header + // However, if the nonquoted param is at the end of the string, there would be no comma + // This is only possible if there are no spaces anywhere + NSRange endRange2 = [header rangeOfString:@" " options:0 range:postStartRange]; + if(endRange2.location != NSNotFound) + { + return nil; + } + else + { + return [header substringWithRange:postStartRange]; + } + } + else + { + NSRange subHeaderRange = NSMakeRange(postStartRangeLocation, endRange.location - postStartRangeLocation); + return [header substringWithRange:subHeaderRange]; + } +} + +@end diff --git a/msext/Class/http/Core/HTTPConnection.h b/msext/Class/http/Core/HTTPConnection.h new file mode 100755 index 0000000..224141c --- /dev/null +++ b/msext/Class/http/Core/HTTPConnection.h @@ -0,0 +1,119 @@ +#import + +@class GCDAsyncSocket; +@class HTTPMessage; +@class HTTPServer; +@class WebSocket; +@protocol HTTPResponse; + + +#define HTTPConnectionDidDieNotification @"HTTPConnectionDidDie" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface HTTPConfig : NSObject +{ + HTTPServer __unsafe_unretained *server; + NSString __strong *documentRoot; + dispatch_queue_t queue; +} + +- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot; +- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot queue:(dispatch_queue_t)q; + +@property (nonatomic, unsafe_unretained, readonly) HTTPServer *server; +@property (nonatomic, strong, readonly) NSString *documentRoot; +@property (nonatomic, readonly) dispatch_queue_t queue; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface HTTPConnection : NSObject +{ + dispatch_queue_t connectionQueue; + GCDAsyncSocket *asyncSocket; + HTTPConfig *config; + + BOOL started; + + HTTPMessage *request; + unsigned int numHeaderLines; + + BOOL sentResponseHeaders; + + NSString *nonce; + long lastNC; + + NSObject *httpResponse; + + NSMutableArray *ranges; + NSMutableArray *ranges_headers; + NSString *ranges_boundry; + int rangeIndex; + + UInt64 requestContentLength; + UInt64 requestContentLengthReceived; + UInt64 requestChunkSize; + UInt64 requestChunkSizeReceived; + + NSMutableArray *responseDataSizes; +} + +- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig; + +- (void)start; +- (void)stop; + +- (void)startConnection; + +- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path; +- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path; + +- (BOOL)isSecureServer; +- (NSArray *)sslIdentityAndCertificates; + +- (BOOL)isPasswordProtected:(NSString *)path; +- (BOOL)useDigestAccessAuthentication; +- (NSString *)realm; +- (NSString *)passwordForUser:(NSString *)username; + +- (NSDictionary *)parseParams:(NSString *)query; +- (NSDictionary *)parseGetParams; + +- (NSString *)requestURI; + +- (NSArray *)directoryIndexFileNames; +- (NSString *)filePathForURI:(NSString *)path; +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory; +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path; +- (WebSocket *)webSocketForURI:(NSString *)path; + +- (void)prepareForBodyWithSize:(UInt64)contentLength; +- (void)processBodyData:(NSData *)postDataChunk; +- (void)finishBody; + +- (void)handleVersionNotSupported:(NSString *)version; +- (void)handleAuthenticationFailed; +- (void)handleResourceNotFound; +- (void)handleInvalidRequest:(NSData *)data; +- (void)handleUnknownMethod:(NSString *)method; + +- (NSData *)preprocessResponse:(HTTPMessage *)response; +- (NSData *)preprocessErrorResponse:(HTTPMessage *)response; + +- (void)finishResponse; + +- (BOOL)shouldDie; +- (void)die; + +@end + +@interface HTTPConnection (AsynchronousHTTPResponse) +- (void)responseHasAvailableData:(NSObject *)sender; +- (void)responseDidAbort:(NSObject *)sender; +@end diff --git a/msext/Class/http/Core/HTTPConnection.m b/msext/Class/http/Core/HTTPConnection.m new file mode 100755 index 0000000..d4ac82f --- /dev/null +++ b/msext/Class/http/Core/HTTPConnection.m @@ -0,0 +1,2708 @@ +#import "GCDAsyncSocket.h" +#import "HTTPServer.h" +#import "HTTPConnection.h" +#import "HTTPMessage.h" +#import "HTTPResponse.h" +#import "HTTPAuthenticationRequest.h" +#import "DDNumber.h" +#import "DDRange.h" +#import "DDData.h" +#import "HTTPFileResponse.h" +#import "HTTPAsyncFileResponse.h" +#import "WebSocket.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; + +// Define chunk size used to read in data for responses +// This is how much data will be read from disk into RAM at a time +#if TARGET_OS_IPHONE + #define READ_CHUNKSIZE (1024 * 256) +#else + #define READ_CHUNKSIZE (1024 * 512) +#endif + +// Define chunk size used to read in POST upload data +#if TARGET_OS_IPHONE + #define POST_CHUNKSIZE (1024 * 256) +#else + #define POST_CHUNKSIZE (1024 * 512) +#endif + +// Define the various timeouts (in seconds) for various parts of the HTTP process +#define TIMEOUT_READ_FIRST_HEADER_LINE 30 +#define TIMEOUT_READ_SUBSEQUENT_HEADER_LINE 30 +#define TIMEOUT_READ_BODY -1 +#define TIMEOUT_WRITE_HEAD 30 +#define TIMEOUT_WRITE_BODY -1 +#define TIMEOUT_WRITE_ERROR 30 +#define TIMEOUT_NONCE 300 + +// Define the various limits +// MAX_HEADER_LINE_LENGTH: Max length (in bytes) of any single line in a header (including \r\n) +// MAX_HEADER_LINES : Max number of lines in a single header (including first GET line) +#define MAX_HEADER_LINE_LENGTH 8190 +#define MAX_HEADER_LINES 100 +// MAX_CHUNK_LINE_LENGTH : For accepting chunked transfer uploads, max length of chunk size line (including \r\n) +#define MAX_CHUNK_LINE_LENGTH 200 + +// Define the various tags we'll use to differentiate what it is we're currently doing +#define HTTP_REQUEST_HEADER 10 +#define HTTP_REQUEST_BODY 11 +#define HTTP_REQUEST_CHUNK_SIZE 12 +#define HTTP_REQUEST_CHUNK_DATA 13 +#define HTTP_REQUEST_CHUNK_TRAILER 14 +#define HTTP_REQUEST_CHUNK_FOOTER 15 +#define HTTP_PARTIAL_RESPONSE 20 +#define HTTP_PARTIAL_RESPONSE_HEADER 21 +#define HTTP_PARTIAL_RESPONSE_BODY 22 +#define HTTP_CHUNKED_RESPONSE_HEADER 30 +#define HTTP_CHUNKED_RESPONSE_BODY 31 +#define HTTP_CHUNKED_RESPONSE_FOOTER 32 +#define HTTP_PARTIAL_RANGE_RESPONSE_BODY 40 +#define HTTP_PARTIAL_RANGES_RESPONSE_BODY 50 +#define HTTP_RESPONSE 90 +#define HTTP_FINAL_RESPONSE 91 + +// A quick note about the tags: +// +// The HTTP_RESPONSE and HTTP_FINAL_RESPONSE are designated tags signalling that the response is completely sent. +// That is, in the onSocket:didWriteDataWithTag: method, if the tag is HTTP_RESPONSE or HTTP_FINAL_RESPONSE, +// it is assumed that the response is now completely sent. +// Use HTTP_RESPONSE if it's the end of a response, and you want to start reading more requests afterwards. +// Use HTTP_FINAL_RESPONSE if you wish to terminate the connection after sending the response. +// +// If you are sending multiple data segments in a custom response, make sure that only the last segment has +// the HTTP_RESPONSE tag. For all other segments prior to the last segment use HTTP_PARTIAL_RESPONSE, or some other +// tag of your own invention. + +@interface HTTPConnection (PrivateAPI) +- (void)startReadingRequest; +- (void)sendResponseHeadersAndBody; +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation HTTPConnection + +static dispatch_queue_t recentNonceQueue; +static NSMutableArray *recentNonces; + +/** + * This method is automatically called (courtesy of Cocoa) before the first instantiation of this class. + * We use it to initialize any static variables. +**/ ++ (void)initialize +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // Initialize class variables + recentNonceQueue = dispatch_queue_create("HTTPConnection-Nonce", NULL); + recentNonces = [[NSMutableArray alloc] initWithCapacity:5]; + }); +} + +/** + * Generates and returns an authentication nonce. + * A nonce is a server-specified string uniquely generated for each 401 response. + * The default implementation uses a single nonce for each session. +**/ ++ (NSString *)generateNonce +{ + // We use the Core Foundation UUID class to generate a nonce value for us + // UUIDs (Universally Unique Identifiers) are 128-bit values guaranteed to be unique. + CFUUIDRef theUUID = CFUUIDCreate(NULL); + NSString *newNonce = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + + // We have to remember that the HTTP protocol is stateless. + // Even though with version 1.1 persistent connections are the norm, they are not guaranteed. + // Thus if we generate a nonce for this connection, + // it should be honored for other connections in the near future. + // + // In fact, this is absolutely necessary in order to support QuickTime. + // When QuickTime makes it's initial connection, it will be unauthorized, and will receive a nonce. + // It then disconnects, and creates a new connection with the nonce, and proper authentication. + // If we don't honor the nonce for the second connection, QuickTime will repeat the process and never connect. + + dispatch_async(recentNonceQueue, ^{ @autoreleasepool { + + [recentNonces addObject:newNonce]; + }}); + + double delayInSeconds = TIMEOUT_NONCE; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); + dispatch_after(popTime, recentNonceQueue, ^{ @autoreleasepool { + + [recentNonces removeObject:newNonce]; + }}); + + return newNonce; +} + +/** + * Returns whether or not the given nonce is in the list of recently generated nonce's. +**/ ++ (BOOL)hasRecentNonce:(NSString *)recentNonce +{ + __block BOOL result = NO; + + dispatch_sync(recentNonceQueue, ^{ @autoreleasepool { + + result = [recentNonces containsObject:recentNonce]; + }}); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Init, Dealloc: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sole Constructor. + * Associates this new HTTP connection with the given AsyncSocket. + * This HTTP connection object will become the socket's delegate and take over responsibility for the socket. +**/ +- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig +{ + if ((self = [super init])) + { + HTTPLogTrace(); + + if (aConfig.queue) + { + connectionQueue = aConfig.queue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(connectionQueue); + #endif + } + else + { + connectionQueue = dispatch_queue_create("HTTPConnection", NULL); + } + + // Take over ownership of the socket + asyncSocket = newSocket; + [asyncSocket setDelegate:self delegateQueue:connectionQueue]; + + // Store configuration + config = aConfig; + + // Initialize lastNC (last nonce count). + // Used with digest access authentication. + // These must increment for each request from the client. + lastNC = 0; + + // Create a new HTTP message + request = [[HTTPMessage alloc] initEmptyRequest]; + + numHeaderLines = 0; + + responseDataSizes = [[NSMutableArray alloc] initWithCapacity:5]; + } + return self; +} + +/** + * Standard Deconstructor. +**/ +- (void)dealloc +{ + HTTPLogTrace(); + + #if !OS_OBJECT_USE_OBJC + dispatch_release(connectionQueue); + #endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; + [asyncSocket disconnect]; + + if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) + { + [httpResponse connectionDidClose]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Method Support +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns whether or not the server will accept messages of a given method + * at a particular URI. +**/ +- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to support methods such as POST. + // + // Things you may want to consider: + // - Does the given path represent a resource that is designed to accept this method? + // - If accepting an upload, is the size of the data being uploaded too big? + // To do this you can check the requestContentLength variable. + // + // For more information, you can always access the HTTPMessage request variable. + // + // You should fall through with a call to [super supportsMethod:method atPath:path] + // + // See also: expectsRequestBodyFromMethod:atPath: + + if ([method isEqualToString:@"GET"]) + return YES; + + if ([method isEqualToString:@"HEAD"]) + return YES; + + return NO; +} + +/** + * Returns whether or not the server expects a body from the given method. + * + * In other words, should the server expect a content-length header and associated body from this method. + * This would be true in the case of a POST, where the client is sending data, + * or for something like PUT where the client is supposed to be uploading a file. +**/ +- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to add support for other methods that expect the client + // to send a body along with the request header. + // + // You should fall through with a call to [super expectsRequestBodyFromMethod:method atPath:path] + // + // See also: supportsMethod:atPath: + + if ([method isEqualToString:@"POST"]) + return YES; + + if ([method isEqualToString:@"PUT"]) + return YES; + + return NO; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark HTTPS +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns whether or not the server is configured to be a secure server. + * In other words, all connections to this server are immediately secured, thus only secure connections are allowed. + * This is the equivalent of having an https server, where it is assumed that all connections must be secure. + * If this is the case, then unsecure connections will not be allowed on this server, and a separate unsecure server + * would need to be run on a separate port in order to support unsecure connections. + * + * Note: In order to support secure connections, the sslIdentityAndCertificates method must be implemented. +**/ +- (BOOL)isSecureServer +{ + HTTPLogTrace(); + + // Override me to create an https server... + + return NO; +} + +/** + * This method is expected to returns an array appropriate for use in kCFStreamSSLCertificates SSL Settings. + * It should be an array of SecCertificateRefs except for the first element in the array, which is a SecIdentityRef. +**/ +- (NSArray *)sslIdentityAndCertificates +{ + HTTPLogTrace(); + + // Override me to provide the proper required SSL identity. + + return nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Password Protection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns whether or not the requested resource is password protected. + * In this generic implementation, nothing is password protected. +**/ +- (BOOL)isPasswordProtected:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to provide password protection... + // You can configure it for the entire server, or based on the current request + + return NO; +} + +/** + * Returns whether or not the authentication challenge should use digest access authentication. + * The alternative is basic authentication. + * + * If at all possible, digest access authentication should be used because it's more secure. + * Basic authentication sends passwords in the clear and should be avoided unless using SSL/TLS. +**/ +- (BOOL)useDigestAccessAuthentication +{ + HTTPLogTrace(); + + // Override me to customize the authentication scheme + // Make sure you understand the security risks of using the weaker basic authentication + + return YES; +} + +/** + * Returns the authentication realm. + * In this generic implmentation, a default realm is used for the entire server. +**/ +- (NSString *)realm +{ + HTTPLogTrace(); + + // Override me to provide a custom realm... + // You can configure it for the entire server, or based on the current request + + return @"defaultRealm@host.com"; +} + +/** + * Returns the password for the given username. +**/ +- (NSString *)passwordForUser:(NSString *)username +{ + HTTPLogTrace(); + + // Override me to provide proper password authentication + // You can configure a password for the entire server, or custom passwords for users and/or resources + + // Security Note: + // A nil password means no access at all. (Such as for user doesn't exist) + // An empty string password is allowed, and will be treated as any other password. (To support anonymous access) + + return nil; +} + +/** + * Returns whether or not the user is properly authenticated. +**/ +- (BOOL)isAuthenticated +{ + HTTPLogTrace(); + + // Extract the authentication information from the Authorization header + HTTPAuthenticationRequest *auth = [[HTTPAuthenticationRequest alloc] initWithRequest:request]; + + if ([self useDigestAccessAuthentication]) + { + // Digest Access Authentication (RFC 2617) + + if(![auth isDigest]) + { + // User didn't send proper digest access authentication credentials + return NO; + } + + if ([auth username] == nil) + { + // The client didn't provide a username + // Most likely they didn't provide any authentication at all + return NO; + } + + NSString *password = [self passwordForUser:[auth username]]; + if (password == nil) + { + // No access allowed (username doesn't exist in system) + return NO; + } + + NSString *url = [[request url] relativeString]; + + if (![url isEqualToString:[auth uri]]) + { + // Requested URL and Authorization URI do not match + // This could be a replay attack + // IE - attacker provides same authentication information, but requests a different resource + return NO; + } + + // The nonce the client provided will most commonly be stored in our local (cached) nonce variable + if (![nonce isEqualToString:[auth nonce]]) + { + // The given nonce may be from another connection + // We need to search our list of recent nonce strings that have been recently distributed + if ([[self class] hasRecentNonce:[auth nonce]]) + { + // Store nonce in local (cached) nonce variable to prevent array searches in the future + nonce = [[auth nonce] copy]; + + // The client has switched to using a different nonce value + // This may happen if the client tries to get a file in a directory with different credentials. + // The previous credentials wouldn't work, and the client would receive a 401 error + // along with a new nonce value. The client then uses this new nonce value and requests the file again. + // Whatever the case may be, we need to reset lastNC, since that variable is on a per nonce basis. + lastNC = 0; + } + else + { + // We have no knowledge of ever distributing such a nonce. + // This could be a replay attack from a previous connection in the past. + return NO; + } + } + + long authNC = strtol([[auth nc] UTF8String], NULL, 16); + + if (authNC <= lastNC) + { + // The nc value (nonce count) hasn't been incremented since the last request. + // This could be a replay attack. + return NO; + } + lastNC = authNC; + + NSString *HA1str = [NSString stringWithFormat:@"%@:%@:%@", [auth username], [auth realm], password]; + NSString *HA2str = [NSString stringWithFormat:@"%@:%@", [request method], [auth uri]]; + + NSString *HA1 = [[[HA1str dataUsingEncoding:NSUTF8StringEncoding] md5Digest] hexStringValue]; + + NSString *HA2 = [[[HA2str dataUsingEncoding:NSUTF8StringEncoding] md5Digest] hexStringValue]; + + NSString *responseStr = [NSString stringWithFormat:@"%@:%@:%@:%@:%@:%@", + HA1, [auth nonce], [auth nc], [auth cnonce], [auth qop], HA2]; + + NSString *response = [[[responseStr dataUsingEncoding:NSUTF8StringEncoding] md5Digest] hexStringValue]; + + return [response isEqualToString:[auth response]]; + } + else + { + // Basic Authentication + + if (![auth isBasic]) + { + // User didn't send proper base authentication credentials + return NO; + } + + // Decode the base 64 encoded credentials + NSString *base64Credentials = [auth base64Credentials]; + + NSData *temp = [[base64Credentials dataUsingEncoding:NSUTF8StringEncoding] base64Decoded]; + + NSString *credentials = [[NSString alloc] initWithData:temp encoding:NSUTF8StringEncoding]; + + // The credentials should be of the form "username:password" + // The username is not allowed to contain a colon + + NSRange colonRange = [credentials rangeOfString:@":"]; + + if (colonRange.length == 0) + { + // Malformed credentials + return NO; + } + + NSString *credUsername = [credentials substringToIndex:colonRange.location]; + NSString *credPassword = [credentials substringFromIndex:(colonRange.location + colonRange.length)]; + + NSString *password = [self passwordForUser:credUsername]; + if (password == nil) + { + // No access allowed (username doesn't exist in system) + return NO; + } + + return [password isEqualToString:credPassword]; + } +} + +/** + * Adds a digest access authentication challenge to the given response. +**/ +- (void)addDigestAuthChallenge:(HTTPMessage *)response +{ + HTTPLogTrace(); + + NSString *authFormat = @"Digest realm=\"%@\", qop=\"auth\", nonce=\"%@\""; + NSString *authInfo = [NSString stringWithFormat:authFormat, [self realm], [[self class] generateNonce]]; + + [response setHeaderField:@"WWW-Authenticate" value:authInfo]; +} + +/** + * Adds a basic authentication challenge to the given response. +**/ +- (void)addBasicAuthChallenge:(HTTPMessage *)response +{ + HTTPLogTrace(); + + NSString *authFormat = @"Basic realm=\"%@\""; + NSString *authInfo = [NSString stringWithFormat:authFormat, [self realm]]; + + [response setHeaderField:@"WWW-Authenticate" value:authInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Core +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Starting point for the HTTP connection after it has been fully initialized (including subclasses). + * This method is called by the HTTP server. +**/ +- (void)start +{ + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + if (!started) + { + started = YES; + [self startConnection]; + } + }}); +} + +/** + * This method is called by the HTTPServer if it is asked to stop. + * The server, in turn, invokes stop on each HTTPConnection instance. +**/ +- (void)stop +{ + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + // Disconnect the socket. + // The socketDidDisconnect delegate method will handle everything else. + [asyncSocket disconnect]; + }}); +} + +/** + * Starting point for the HTTP connection. +**/ +- (void)startConnection +{ + // Override me to do any custom work before the connection starts. + // + // Be sure to invoke [super startConnection] when you're done. + + HTTPLogTrace(); + + if ([self isSecureServer]) + { + // We are configured to be an HTTPS server. + // That is, we secure via SSL/TLS the connection prior to any communication. + + NSArray *certificates = [self sslIdentityAndCertificates]; + + if ([certificates count] > 0) + { + // All connections are assumed to be secure. Only secure connections are allowed on this server. + NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:3]; + + // Configure this connection as the server + [settings setObject:[NSNumber numberWithBool:YES] + forKey:(NSString *)kCFStreamSSLIsServer]; + + [settings setObject:certificates + forKey:(NSString *)kCFStreamSSLCertificates]; + + // Configure this connection to use the highest possible SSL level + [settings setObject:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL + forKey:(NSString *)kCFStreamSSLLevel]; + + [asyncSocket startTLS:settings]; + } + } + + [self startReadingRequest]; +} + +/** + * Starts reading an HTTP request. +**/ +- (void)startReadingRequest +{ + HTTPLogTrace(); + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_FIRST_HEADER_LINE + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_HEADER]; +} + +/** + * Parses the given query string. + * + * For example, if the query is "q=John%20Mayer%20Trio&num=50" + * then this method would return the following dictionary: + * { + * q = "John Mayer Trio" + * num = "50" + * } +**/ +- (NSDictionary *)parseParams:(NSString *)query +{ + NSArray *components = [query componentsSeparatedByString:@"&"]; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[components count]]; + + NSUInteger i; + for (i = 0; i < [components count]; i++) + { + NSString *component = [components objectAtIndex:i]; + if ([component length] > 0) + { + NSRange range = [component rangeOfString:@"="]; + if (range.location != NSNotFound) + { + NSString *escapedKey = [component substringToIndex:(range.location + 0)]; + NSString *escapedValue = [component substringFromIndex:(range.location + 1)]; + + if ([escapedKey length] > 0) + { + CFStringRef k, v; + + k = CFURLCreateStringByReplacingPercentEscapes(NULL, (__bridge CFStringRef)escapedKey, CFSTR("")); + v = CFURLCreateStringByReplacingPercentEscapes(NULL, (__bridge CFStringRef)escapedValue, CFSTR("")); + + NSString *key, *value; + + key = (__bridge_transfer NSString *)k; + value = (__bridge_transfer NSString *)v; + + if (key) + { + if (value) + [result setObject:value forKey:key]; + else + [result setObject:[NSNull null] forKey:key]; + } + } + } + } + } + + return result; +} + +/** + * Parses the query variables in the request URI. + * + * For example, if the request URI was "/search.html?q=John%20Mayer%20Trio&num=50" + * then this method would return the following dictionary: + * { + * q = "John Mayer Trio" + * num = "50" + * } +**/ +- (NSDictionary *)parseGetParams +{ + if(![request isHeaderComplete]) return nil; + + NSDictionary *result = nil; + + NSURL *url = [request url]; + if(url) + { + NSString *query = [url query]; + if (query) + { + result = [self parseParams:query]; + } + } + + return result; +} + +/** + * Attempts to parse the given range header into a series of sequential non-overlapping ranges. + * If successfull, the variables 'ranges' and 'rangeIndex' will be updated, and YES will be returned. + * Otherwise, NO is returned, and the range request should be ignored. + **/ +- (BOOL)parseRangeRequest:(NSString *)rangeHeader withContentLength:(UInt64)contentLength +{ + HTTPLogTrace(); + + // Examples of byte-ranges-specifier values (assuming an entity-body of length 10000): + // + // - The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499 + // + // - The second 500 bytes (byte offsets 500-999, inclusive): bytes=500-999 + // + // - The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 + // + // - Or bytes=9500- + // + // - The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 + // + // - Several legal but not canonical specifications of the second 500 bytes (byte offsets 500-999, inclusive): + // bytes=500-600,601-999 + // bytes=500-700,601-999 + // + + NSRange eqsignRange = [rangeHeader rangeOfString:@"="]; + + if(eqsignRange.location == NSNotFound) return NO; + + NSUInteger tIndex = eqsignRange.location; + NSUInteger fIndex = eqsignRange.location + eqsignRange.length; + + NSMutableString *rangeType = [[rangeHeader substringToIndex:tIndex] mutableCopy]; + NSMutableString *rangeValue = [[rangeHeader substringFromIndex:fIndex] mutableCopy]; + + CFStringTrimWhitespace((__bridge CFMutableStringRef)rangeType); + CFStringTrimWhitespace((__bridge CFMutableStringRef)rangeValue); + + if([rangeType caseInsensitiveCompare:@"bytes"] != NSOrderedSame) return NO; + + NSArray *rangeComponents = [rangeValue componentsSeparatedByString:@","]; + + if([rangeComponents count] == 0) return NO; + + ranges = [[NSMutableArray alloc] initWithCapacity:[rangeComponents count]]; + + rangeIndex = 0; + + // Note: We store all range values in the form of DDRange structs, wrapped in NSValue objects. + // Since DDRange consists of UInt64 values, the range extends up to 16 exabytes. + + NSUInteger i; + for (i = 0; i < [rangeComponents count]; i++) + { + NSString *rangeComponent = [rangeComponents objectAtIndex:i]; + + NSRange dashRange = [rangeComponent rangeOfString:@"-"]; + + if (dashRange.location == NSNotFound) + { + // We're dealing with an individual byte number + + UInt64 byteIndex; + if(![NSNumber parseString:rangeComponent intoUInt64:&byteIndex]) return NO; + + if(byteIndex >= contentLength) return NO; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(byteIndex, 1)]]; + } + else + { + // We're dealing with a range of bytes + + tIndex = dashRange.location; + fIndex = dashRange.location + dashRange.length; + + NSString *r1str = [rangeComponent substringToIndex:tIndex]; + NSString *r2str = [rangeComponent substringFromIndex:fIndex]; + + UInt64 r1, r2; + + BOOL hasR1 = [NSNumber parseString:r1str intoUInt64:&r1]; + BOOL hasR2 = [NSNumber parseString:r2str intoUInt64:&r2]; + + if (!hasR1) + { + // We're dealing with a "-[#]" range + // + // r2 is the number of ending bytes to include in the range + + if(!hasR2) return NO; + if(r2 > contentLength) return NO; + + UInt64 startIndex = contentLength - r2; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(startIndex, r2)]]; + } + else if (!hasR2) + { + // We're dealing with a "[#]-" range + // + // r1 is the starting index of the range, which goes all the way to the end + + if(r1 >= contentLength) return NO; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(r1, contentLength - r1)]]; + } + else + { + // We're dealing with a normal "[#]-[#]" range + // + // Note: The range is inclusive. So 0-1 has a length of 2 bytes. + + if(r1 > r2) return NO; + if(r2 >= contentLength) return NO; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(r1, r2 - r1 + 1)]]; + } + } + } + + if([ranges count] == 0) return NO; + + // Now make sure none of the ranges overlap + + for (i = 0; i < [ranges count] - 1; i++) + { + DDRange range1 = [[ranges objectAtIndex:i] ddrangeValue]; + + NSUInteger j; + for (j = i+1; j < [ranges count]; j++) + { + DDRange range2 = [[ranges objectAtIndex:j] ddrangeValue]; + + DDRange iRange = DDIntersectionRange(range1, range2); + + if(iRange.length != 0) + { + return NO; + } + } + } + + // Sort the ranges + + [ranges sortUsingSelector:@selector(ddrangeCompare:)]; + + return YES; +} + +- (NSString *)requestURI +{ + if(request == nil) return nil; + + return [[request url] relativeString]; +} + +/** + * This method is called after a full HTTP request has been received. + * The current request is in the HTTPMessage request variable. +**/ +- (void)replyToHTTPRequest +{ + HTTPLogTrace(); + + if (HTTP_LOG_VERBOSE) + { + NSData *tempData = [request messageData]; + + NSString *tempStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding]; + HTTPLogVerbose(@"%@[%p]: Received HTTP request:\n%@", THIS_FILE, self, tempStr); + } + + // Check the HTTP version + // We only support version 1.0 and 1.1 + + NSString *version = [request version]; + if (![version isEqualToString:HTTPVersion1_1] && ![version isEqualToString:HTTPVersion1_0]) + { + [self handleVersionNotSupported:version]; + return; + } + + // Extract requested URI + NSString *uri = [self requestURI]; + + // Check for WebSocket request + if ([WebSocket isWebSocketRequest:request]) + { + HTTPLogVerbose(@"isWebSocket"); + + WebSocket *ws = [self webSocketForURI:uri]; + + if (ws == nil) + { + [self handleResourceNotFound]; + } + else + { + [ws start]; + + [[config server] addWebSocket:ws]; + + // The WebSocket should now be the delegate of the underlying socket. + // But gracefully handle the situation if it forgot. + if ([asyncSocket delegate] == self) + { + HTTPLogWarn(@"%@[%p]: WebSocket forgot to set itself as socket delegate", THIS_FILE, self); + + // Disconnect the socket. + // The socketDidDisconnect delegate method will handle everything else. + [asyncSocket disconnect]; + } + else + { + // The WebSocket is using the socket, + // so make sure we don't disconnect it in the dealloc method. + asyncSocket = nil; + + [self die]; + + // Note: There is a timing issue here that should be pointed out. + // + // A bug that existed in previous versions happend like so: + // - We invoked [self die] + // - This caused us to get released, and our dealloc method to start executing + // - Meanwhile, AsyncSocket noticed a disconnect, and began to dispatch a socketDidDisconnect at us + // - The dealloc method finishes execution, and our instance gets freed + // - The socketDidDisconnect gets run, and a crash occurs + // + // So the issue we want to avoid is releasing ourself when there is a possibility + // that AsyncSocket might be gearing up to queue a socketDidDisconnect for us. + // + // In this particular situation notice that we invoke [asyncSocket delegate]. + // This method is synchronous concerning AsyncSocket's internal socketQueue. + // Which means we can be sure, when it returns, that AsyncSocket has already + // queued any delegate methods for us if it was going to. + // And if the delegate methods are queued, then we've been properly retained. + // Meaning we won't get released / dealloc'd until the delegate method has finished executing. + // + // In this rare situation, the die method will get invoked twice. + } + } + + return; + } + + // Check Authentication (if needed) + // If not properly authenticated for resource, issue Unauthorized response + if ([self isPasswordProtected:uri] && ![self isAuthenticated]) + { + [self handleAuthenticationFailed]; + return; + } + + // Extract the method + NSString *method = [request method]; + + // Note: We already checked to ensure the method was supported in onSocket:didReadData:withTag: + + // Respond properly to HTTP 'GET' and 'HEAD' commands + httpResponse = [self httpResponseForMethod:method URI:uri]; + + if (httpResponse == nil) + { + [self handleResourceNotFound]; + return; + } + + [self sendResponseHeadersAndBody]; +} + +/** + * Prepares a single-range response. + * + * Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it. +**/ +- (HTTPMessage *)newUniRangeResponse:(UInt64)contentLength +{ + HTTPLogTrace(); + + // Status Code 206 - Partial Content + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1]; + + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", range.length]; + [response setHeaderField:@"Content-Length" value:contentLengthStr]; + + NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1]; + NSString *contentRangeStr = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength]; + [response setHeaderField:@"Content-Range" value:contentRangeStr]; + + return response; +} + +/** + * Prepares a multi-range response. + * + * Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it. +**/ +- (HTTPMessage *)newMultiRangeResponse:(UInt64)contentLength +{ + HTTPLogTrace(); + + // Status Code 206 - Partial Content + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1]; + + // We have to send each range using multipart/byteranges + // So each byterange has to be prefix'd and suffix'd with the boundry + // Example: + // + // HTTP/1.1 206 Partial Content + // Content-Length: 220 + // Content-Type: multipart/byteranges; boundary=4554d24e986f76dd6 + // + // + // --4554d24e986f76dd6 + // Content-Range: bytes 0-25/4025 + // + // [...] + // --4554d24e986f76dd6 + // Content-Range: bytes 3975-4024/4025 + // + // [...] + // --4554d24e986f76dd6-- + + ranges_headers = [[NSMutableArray alloc] initWithCapacity:[ranges count]]; + + CFUUIDRef theUUID = CFUUIDCreate(NULL); + ranges_boundry = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + + NSString *startingBoundryStr = [NSString stringWithFormat:@"\r\n--%@\r\n", ranges_boundry]; + NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry]; + + UInt64 actualContentLength = 0; + + NSUInteger i; + for (i = 0; i < [ranges count]; i++) + { + DDRange range = [[ranges objectAtIndex:i] ddrangeValue]; + + NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1]; + NSString *contentRangeVal = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength]; + NSString *contentRangeStr = [NSString stringWithFormat:@"Content-Range: %@\r\n\r\n", contentRangeVal]; + + NSString *fullHeader = [startingBoundryStr stringByAppendingString:contentRangeStr]; + NSData *fullHeaderData = [fullHeader dataUsingEncoding:NSUTF8StringEncoding]; + + [ranges_headers addObject:fullHeaderData]; + + actualContentLength += [fullHeaderData length]; + actualContentLength += range.length; + } + + NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding]; + + actualContentLength += [endingBoundryData length]; + + NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", actualContentLength]; + [response setHeaderField:@"Content-Length" value:contentLengthStr]; + + NSString *contentTypeStr = [NSString stringWithFormat:@"multipart/byteranges; boundary=%@", ranges_boundry]; + [response setHeaderField:@"Content-Type" value:contentTypeStr]; + + return response; +} + +/** + * Returns the chunk size line that must precede each chunk of data when using chunked transfer encoding. + * This consists of the size of the data, in hexadecimal, followed by a CRLF. +**/ +- (NSData *)chunkedTransferSizeLineForLength:(NSUInteger)length +{ + return [[NSString stringWithFormat:@"%lx\r\n", (unsigned long)length] dataUsingEncoding:NSUTF8StringEncoding]; +} + +/** + * Returns the data that signals the end of a chunked transfer. +**/ +- (NSData *)chunkedTransferFooter +{ + // Each data chunk is preceded by a size line (in hex and including a CRLF), + // followed by the data itself, followed by another CRLF. + // After every data chunk has been sent, a zero size line is sent, + // followed by optional footer (which are just more headers), + // and followed by a CRLF on a line by itself. + + return [@"\r\n0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (void)sendResponseHeadersAndBody +{ + if ([httpResponse respondsToSelector:@selector(delayResponseHeaders)]) + { + if ([httpResponse delayResponseHeaders]) + { + return; + } + } + + BOOL isChunked = NO; + + if ([httpResponse respondsToSelector:@selector(isChunked)]) + { + isChunked = [httpResponse isChunked]; + } + + // If a response is "chunked", this simply means the HTTPResponse object + // doesn't know the content-length in advance. + + UInt64 contentLength = 0; + + if (!isChunked) + { + contentLength = [httpResponse contentLength]; + } + + // Check for specific range request + NSString *rangeHeader = [request headerField:@"Range"]; + + BOOL isRangeRequest = NO; + + // If the response is "chunked" then we don't know the exact content-length. + // This means we'll be unable to process any range requests. + // This is because range requests might include a range like "give me the last 100 bytes" + + if (!isChunked && rangeHeader) + { + if ([self parseRangeRequest:rangeHeader withContentLength:contentLength]) + { + isRangeRequest = YES; + } + } + + HTTPMessage *response; + + if (!isRangeRequest) + { + // Create response + // Default status code: 200 - OK + NSInteger status = 200; + + if ([httpResponse respondsToSelector:@selector(status)]) + { + status = [httpResponse status]; + } + response = [[HTTPMessage alloc] initResponseWithStatusCode:status description:nil version:HTTPVersion1_1]; + + if (isChunked) + { + [response setHeaderField:@"Transfer-Encoding" value:@"chunked"]; + } + else + { + NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", contentLength]; + [response setHeaderField:@"Content-Length" value:contentLengthStr]; + } + } + else + { + if ([ranges count] == 1) + { + response = [self newUniRangeResponse:contentLength]; + } + else + { + response = [self newMultiRangeResponse:contentLength]; + } + } + + BOOL isZeroLengthResponse = !isChunked && (contentLength == 0); + + // If they issue a 'HEAD' command, we don't have to include the file + // If they issue a 'GET' command, we need to include the file + + if ([[request method] isEqualToString:@"HEAD"] || isZeroLengthResponse) + { + NSData *responseData = [self preprocessResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + + sentResponseHeaders = YES; + } + else + { + // Write the header response + NSData *responseData = [self preprocessResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; + + sentResponseHeaders = YES; + + // Now we need to send the body of the response + if (!isRangeRequest) + { + // Regular request + NSData *data = [httpResponse readDataOfLength:READ_CHUNKSIZE]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + if (isChunked) + { + NSData *chunkSize = [self chunkedTransferSizeLineForLength:[data length]]; + [asyncSocket writeData:chunkSize withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_HEADER]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_CHUNKED_RESPONSE_BODY]; + + if ([httpResponse isDone]) + { + NSData *footer = [self chunkedTransferFooter]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + } + else + { + NSData *footer = [GCDAsyncSocket CRLFData]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_FOOTER]; + } + } + else + { + long tag = [httpResponse isDone] ? HTTP_RESPONSE : HTTP_PARTIAL_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } + } + else + { + // Client specified a byte range in request + + if ([ranges count] == 1) + { + // Client is requesting a single range + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + [httpResponse setOffset:range.location]; + + NSUInteger bytesToRead = range.length < READ_CHUNKSIZE ? (NSUInteger)range.length : READ_CHUNKSIZE; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + long tag = [data length] == range.length ? HTTP_RESPONSE : HTTP_PARTIAL_RANGE_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } + else + { + // Client is requesting multiple ranges + // We have to send each range using multipart/byteranges + + // Write range header + NSData *rangeHeaderData = [ranges_headers objectAtIndex:0]; + [asyncSocket writeData:rangeHeaderData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; + + // Start writing range body + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + [httpResponse setOffset:range.location]; + + NSUInteger bytesToRead = range.length < READ_CHUNKSIZE ? (NSUInteger)range.length : READ_CHUNKSIZE; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; + } + } + } + } + +} + +/** + * Returns the number of bytes of the http response body that are sitting in asyncSocket's write queue. + * + * We keep track of this information in order to keep our memory footprint low while + * working with asynchronous HTTPResponse objects. +**/ +- (NSUInteger)writeQueueSize +{ + NSUInteger result = 0; + + NSUInteger i; + for(i = 0; i < [responseDataSizes count]; i++) + { + result += [[responseDataSizes objectAtIndex:i] unsignedIntegerValue]; + } + + return result; +} + +/** + * Sends more data, if needed, without growing the write queue over its approximate size limit. + * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. + * + * This method should only be called for standard (non-range) responses. +**/ +- (void)continueSendingStandardResponseBody +{ + HTTPLogTrace(); + + // This method is called when either asyncSocket has finished writing one of the response data chunks, + // or when an asynchronous HTTPResponse object informs us that it has more available data for us to send. + // In the case of the asynchronous HTTPResponse, we don't want to blindly grab the new data, + // and shove it onto asyncSocket's write queue. + // Doing so could negatively affect the memory footprint of the application. + // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. + // + // Note that this does not affect the rate at which the HTTPResponse object may generate data. + // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. + // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely + // use the calls to readDataOfLength as an indication to start generating more data. + // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate + // at which the socket is able to send it. + + NSUInteger writeQueueSize = [self writeQueueSize]; + + if(writeQueueSize >= READ_CHUNKSIZE) return; + + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSData *data = [httpResponse readDataOfLength:available]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + BOOL isChunked = NO; + + if ([httpResponse respondsToSelector:@selector(isChunked)]) + { + isChunked = [httpResponse isChunked]; + } + + if (isChunked) + { + NSData *chunkSize = [self chunkedTransferSizeLineForLength:[data length]]; + [asyncSocket writeData:chunkSize withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_HEADER]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_CHUNKED_RESPONSE_BODY]; + + if([httpResponse isDone]) + { + NSData *footer = [self chunkedTransferFooter]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + } + else + { + NSData *footer = [GCDAsyncSocket CRLFData]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_FOOTER]; + } + } + else + { + long tag = [httpResponse isDone] ? HTTP_RESPONSE : HTTP_PARTIAL_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } +} + +/** + * Sends more data, if needed, without growing the write queue over its approximate size limit. + * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. + * + * This method should only be called for single-range responses. +**/ +- (void)continueSendingSingleRangeResponseBody +{ + HTTPLogTrace(); + + // This method is called when either asyncSocket has finished writing one of the response data chunks, + // or when an asynchronous response informs us that is has more available data for us to send. + // In the case of the asynchronous response, we don't want to blindly grab the new data, + // and shove it onto asyncSocket's write queue. + // Doing so could negatively affect the memory footprint of the application. + // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. + // + // Note that this does not affect the rate at which the HTTPResponse object may generate data. + // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. + // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely + // use the calls to readDataOfLength as an indication to start generating more data. + // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate + // at which the socket is able to send it. + + NSUInteger writeQueueSize = [self writeQueueSize]; + + if(writeQueueSize >= READ_CHUNKSIZE) return; + + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + UInt64 offset = [httpResponse offset]; + UInt64 bytesRead = offset - range.location; + UInt64 bytesLeft = range.length - bytesRead; + + if (bytesLeft > 0) + { + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSUInteger bytesToRead = bytesLeft < available ? (NSUInteger)bytesLeft : available; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + long tag = [data length] == bytesLeft ? HTTP_RESPONSE : HTTP_PARTIAL_RANGE_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } +} + +/** + * Sends more data, if needed, without growing the write queue over its approximate size limit. + * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. + * + * This method should only be called for multi-range responses. +**/ +- (void)continueSendingMultiRangeResponseBody +{ + HTTPLogTrace(); + + // This method is called when either asyncSocket has finished writing one of the response data chunks, + // or when an asynchronous HTTPResponse object informs us that is has more available data for us to send. + // In the case of the asynchronous HTTPResponse, we don't want to blindly grab the new data, + // and shove it onto asyncSocket's write queue. + // Doing so could negatively affect the memory footprint of the application. + // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. + // + // Note that this does not affect the rate at which the HTTPResponse object may generate data. + // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. + // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely + // use the calls to readDataOfLength as an indication to start generating more data. + // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate + // at which the socket is able to send it. + + NSUInteger writeQueueSize = [self writeQueueSize]; + + if(writeQueueSize >= READ_CHUNKSIZE) return; + + DDRange range = [[ranges objectAtIndex:rangeIndex] ddrangeValue]; + + UInt64 offset = [httpResponse offset]; + UInt64 bytesRead = offset - range.location; + UInt64 bytesLeft = range.length - bytesRead; + + if (bytesLeft > 0) + { + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSUInteger bytesToRead = bytesLeft < available ? (NSUInteger)bytesLeft : available; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; + } + } + else + { + if (++rangeIndex < [ranges count]) + { + // Write range header + NSData *rangeHeader = [ranges_headers objectAtIndex:rangeIndex]; + [asyncSocket writeData:rangeHeader withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; + + // Start writing range body + range = [[ranges objectAtIndex:rangeIndex] ddrangeValue]; + + [httpResponse setOffset:range.location]; + + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSUInteger bytesToRead = range.length < available ? (NSUInteger)range.length : available; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; + } + } + else + { + // We're not done yet - we still have to send the closing boundry tag + NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry]; + NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding]; + + [asyncSocket writeData:endingBoundryData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Responses +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns an array of possible index pages. + * For example: {"index.html", "index.htm"} +**/ +- (NSArray *)directoryIndexFileNames +{ + HTTPLogTrace(); + + // Override me to support other index pages. + + return [NSArray arrayWithObjects:@"index.html", @"index.htm", nil]; +} + +- (NSString *)filePathForURI:(NSString *)path +{ + return [self filePathForURI:path allowDirectory:NO]; +} + +/** + * Converts relative URI path into full file-system path. +**/ +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory +{ + HTTPLogTrace(); + + // Override me to perform custom path mapping. + // For example you may want to use a default file other than index.html, or perhaps support multiple types. + + NSString *documentRoot = [config documentRoot]; + + // Part 0: Validate document root setting. + // + // If there is no configured documentRoot, + // then it makes no sense to try to return anything. + + if (documentRoot == nil) + { + HTTPLogWarn(@"%@[%p]: No configured document root", THIS_FILE, self); + return nil; + } + + // Part 1: Strip parameters from the url + // + // E.g.: /page.html?q=22&var=abc -> /page.html + + NSURL *docRoot = [NSURL fileURLWithPath:documentRoot isDirectory:YES]; + if (docRoot == nil) + { + HTTPLogWarn(@"%@[%p]: Document root is invalid file path", THIS_FILE, self); + return nil; + } + + NSString *relativePath = [[NSURL URLWithString:path relativeToURL:docRoot] relativePath]; + + // Part 2: Append relative path to document root (base path) + // + // E.g.: relativePath="/images/icon.png" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Sites/images/icon.png" + // + // We also standardize the path. + // + // E.g.: "Users/robbie/Sites/images/../index.html" -> "/Users/robbie/Sites/index.html" + + NSString *fullPath = [[documentRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath]; + + if ([relativePath isEqualToString:@"/"]) + { + fullPath = [fullPath stringByAppendingString:@"/"]; + } + + // Part 3: Prevent serving files outside the document root. + // + // Sneaky requests may include ".." in the path. + // + // E.g.: relativePath="../Documents/TopSecret.doc" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Documents/TopSecret.doc" + // + // E.g.: relativePath="../Sites_Secret/TopSecret.doc" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Sites_Secret/TopSecret" + + if (![documentRoot hasSuffix:@"/"]) + { + documentRoot = [documentRoot stringByAppendingString:@"/"]; + } + + if (![fullPath hasPrefix:documentRoot]) + { + HTTPLogWarn(@"%@[%p]: Request for file outside document root", THIS_FILE, self); + return nil; + } + + // Part 4: Search for index page if path is pointing to a directory + if (!allowDirectory) + { + BOOL isDir = NO; + if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) + { + NSArray *indexFileNames = [self directoryIndexFileNames]; + + for (NSString *indexFileName in indexFileNames) + { + NSString *indexFilePath = [fullPath stringByAppendingPathComponent:indexFileName]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:indexFilePath isDirectory:&isDir] && !isDir) + { + return indexFilePath; + } + } + + // No matching index files found in directory + return nil; + } + } + + return fullPath; +} + +/** + * This method is called to get a response for a request. + * You may return any object that adopts the HTTPResponse protocol. + * The HTTPServer comes with two such classes: HTTPFileResponse and HTTPDataResponse. + * HTTPFileResponse is a wrapper for an NSFileHandle object, and is the preferred way to send a file response. + * HTTPDataResponse is a wrapper for an NSData object, and may be used to send a custom response. +**/ +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to provide custom responses. + + NSString *filePath = [self filePathForURI:path allowDirectory:NO]; + + BOOL isDir = NO; + + if (filePath && [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDir] && !isDir) + { + return [[HTTPFileResponse alloc] initWithFilePath:filePath forConnection:self]; + + // Use me instead for asynchronous file IO. + // Generally better for larger files. + + // return [[[HTTPAsyncFileResponse alloc] initWithFilePath:filePath forConnection:self] autorelease]; + } + + return nil; +} + +- (WebSocket *)webSocketForURI:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to provide custom WebSocket responses. + // To do so, simply override the base WebSocket implementation, and add your custom functionality. + // Then return an instance of your custom WebSocket here. + // + // For example: + // + // if ([path isEqualToString:@"/myAwesomeWebSocketStream"]) + // { + // return [[[MyWebSocket alloc] initWithRequest:request socket:asyncSocket] autorelease]; + // } + // + // return [super webSocketForURI:path]; + + return nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Uploads +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called after receiving all HTTP headers, but before reading any of the request body. +**/ +- (void)prepareForBodyWithSize:(UInt64)contentLength +{ + // Override me to allocate buffers, file handles, etc. +} + +/** + * This method is called to handle data read from a POST / PUT. + * The given data is part of the request body. +**/ +- (void)processBodyData:(NSData *)postDataChunk +{ + // Override me to do something useful with a POST / PUT. + // If the post is small, such as a simple form, you may want to simply append the data to the request. + // If the post is big, such as a file upload, you may want to store the file to disk. + // + // Remember: In order to support LARGE POST uploads, the data is read in chunks. + // This prevents a 50 MB upload from being stored in RAM. + // The size of the chunks are limited by the POST_CHUNKSIZE definition. + // Therefore, this method may be called multiple times for the same POST request. +} + +/** + * This method is called after the request body has been fully read but before the HTTP request is processed. +**/ +- (void)finishBody +{ + // Override me to perform any final operations on an upload. + // For example, if you were saving the upload to disk this would be + // the hook to flush any pending data to disk and maybe close the file. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called if the HTML version is other than what is supported +**/ +- (void)handleVersionNotSupported:(NSString *)version +{ + // Override me for custom error handling of unsupported http version responses + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogWarn(@"HTTP Server: Error 505 - Version Not Supported: %@ (%@)", version, [self requestURI]); + + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:505 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; + +} + +/** + * Called if the authentication information was required and absent, or if authentication failed. +**/ +- (void)handleAuthenticationFailed +{ + // Override me for custom handling of authentication challenges + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogInfo(@"HTTP Server: Error 401 - Unauthorized (%@)", [self requestURI]); + + // Status Code 401 - Unauthorized + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:401 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + + if ([self useDigestAccessAuthentication]) + { + [self addDigestAuthChallenge:response]; + } + else + { + [self addBasicAuthChallenge:response]; + } + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; + +} + +/** + * Called if we receive some sort of malformed HTTP request. + * The data parameter is the invalid HTTP header line, including CRLF, as read from GCDAsyncSocket. + * The data parameter may also be nil if the request as a whole was invalid, such as a POST with no Content-Length. +**/ +- (void)handleInvalidRequest:(NSData *)data +{ + // Override me for custom error handling of invalid HTTP requests + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogWarn(@"HTTP Server: Error 400 - Bad Request (%@)", [self requestURI]); + + // Status Code 400 - Bad Request + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:400 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + [response setHeaderField:@"Connection" value:@"close"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; + + + // Note: We used the HTTP_FINAL_RESPONSE tag to disconnect after the response is sent. + // We do this because we couldn't parse the request, + // so we won't be able to recover and move on to another request afterwards. + // In other words, we wouldn't know where the first request ends and the second request begins. +} + +/** + * Called if we receive a HTTP request with a method other than GET or HEAD. +**/ +- (void)handleUnknownMethod:(NSString *)method +{ + // Override me for custom error handling of 405 method not allowed responses. + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + // + // See also: supportsMethod:atPath: + + HTTPLogWarn(@"HTTP Server: Error 405 - Method Not Allowed: %@ (%@)", method, [self requestURI]); + + // Status code 405 - Method Not Allowed + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:405 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + [response setHeaderField:@"Connection" value:@"close"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; + + + // Note: We used the HTTP_FINAL_RESPONSE tag to disconnect after the response is sent. + // We do this because the method may include an http body. + // Since we can't be sure, we should close the connection. +} + +/** + * Called if we're unable to find the requested resource. +**/ +- (void)handleResourceNotFound +{ + // Override me for custom error handling of 404 not found responses + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogInfo(@"HTTP Server: Error 404 - Not Found (%@)", [self requestURI]); + + // Status Code 404 - Not Found + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:404 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Headers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Gets the current date and time, formatted properly (according to RFC) for insertion into an HTTP header. +**/ +- (NSString *)dateAsString:(NSDate *)date +{ + // From Apple's Documentation (Data Formatting Guide -> Date Formatters -> Cache Formatters for Efficiency): + // + // "Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, + // it is typically more efficient to cache a single instance than to create and dispose of multiple instances. + // One approach is to use a static variable." + // + // This was discovered to be true in massive form via issue #46: + // + // "Was doing some performance benchmarking using instruments and httperf. Using this single optimization + // I got a 26% speed improvement - from 1000req/sec to 3800req/sec. Not insignificant. + // The culprit? Why, NSDateFormatter, of course!" + // + // Thus, we are using a static NSDateFormatter here. + + static NSDateFormatter *df; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // Example: Sun, 06 Nov 1994 08:49:37 GMT + + df = [[NSDateFormatter alloc] init]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; + [df setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]]; + [df setDateFormat:@"EEE, dd MMM y HH:mm:ss 'GMT'"]; + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]]; + + // For some reason, using zzz in the format string produces GMT+00:00 + }); + + return [df stringFromDate:date]; +} + +/** + * This method is called immediately prior to sending the response headers. + * This method adds standard header fields, and then converts the response to an NSData object. +**/ +- (NSData *)preprocessResponse:(HTTPMessage *)response +{ + HTTPLogTrace(); + + // Override me to customize the response headers + // You'll likely want to add your own custom headers, and then return [super preprocessResponse:response] + + // Add standard headers + NSString *now = [self dateAsString:[NSDate date]]; + [response setHeaderField:@"Date" value:now]; + + // Add server capability headers + [response setHeaderField:@"Accept-Ranges" value:@"bytes"]; + + // Add optional response headers + if ([httpResponse respondsToSelector:@selector(httpHeaders)]) + { + NSDictionary *responseHeaders = [httpResponse httpHeaders]; + + NSEnumerator *keyEnumerator = [responseHeaders keyEnumerator]; + NSString *key; + + while ((key = [keyEnumerator nextObject])) + { + NSString *value = [responseHeaders objectForKey:key]; + + [response setHeaderField:key value:value]; + } + } + + return [response messageData]; +} + +/** + * This method is called immediately prior to sending the response headers (for an error). + * This method adds standard header fields, and then converts the response to an NSData object. +**/ +- (NSData *)preprocessErrorResponse:(HTTPMessage *)response +{ + HTTPLogTrace(); + + // Override me to customize the error response headers + // You'll likely want to add your own custom headers, and then return [super preprocessErrorResponse:response] + // + // Notes: + // You can use [response statusCode] to get the type of error. + // You can use [response setBody:data] to add an optional HTML body. + // If you add a body, don't forget to update the Content-Length. + // + // if ([response statusCode] == 404) + // { + // NSString *msg = @"Error 404 - Not Found"; + // NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding]; + // + // [response setBody:msgData]; + // + // NSString *contentLengthStr = [NSString stringWithFormat:@"%lu", (unsigned long)[msgData length]]; + // [response setHeaderField:@"Content-Length" value:contentLengthStr]; + // } + + // Add standard headers + NSString *now = [self dateAsString:[NSDate date]]; + [response setHeaderField:@"Date" value:now]; + + // Add server capability headers + [response setHeaderField:@"Accept-Ranges" value:@"bytes"]; + + // Add optional response headers + if ([httpResponse respondsToSelector:@selector(httpHeaders)]) + { + NSDictionary *responseHeaders = [httpResponse httpHeaders]; + + NSEnumerator *keyEnumerator = [responseHeaders keyEnumerator]; + NSString *key; + + while((key = [keyEnumerator nextObject])) + { + NSString *value = [responseHeaders objectForKey:key]; + + [response setHeaderField:key value:value]; + } + } + + return [response messageData]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark GCDAsyncSocket Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called after the socket has successfully read data from the stream. + * Remember that this method will only be called after the socket reaches a CRLF, or after it's read the proper length. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)data withTag:(long)tag +{ + if (tag == HTTP_REQUEST_HEADER) + { + // Append the header line to the http message + BOOL result = [request appendData:data]; + if (!result) + { + HTTPLogWarn(@"%@[%p]: Malformed request", THIS_FILE, self); + + [self handleInvalidRequest:data]; + } + else if (![request isHeaderComplete]) + { + // We don't have a complete header yet + // That is, we haven't yet received a CRLF on a line by itself, indicating the end of the header + if (++numHeaderLines > MAX_HEADER_LINES) + { + // Reached the maximum amount of header lines in a single HTTP request + // This could be an attempted DOS attack + [asyncSocket disconnect]; + + // Explictly return to ensure we don't do anything after the socket disconnect + return; + } + else + { + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_SUBSEQUENT_HEADER_LINE + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_HEADER]; + } + } + else + { + // We have an entire HTTP request header from the client + + // Extract the method (such as GET, HEAD, POST, etc) + NSString *method = [request method]; + + // Extract the uri (such as "/index.html") + NSString *uri = [self requestURI]; + + // Check for a Transfer-Encoding field + NSString *transferEncoding = [request headerField:@"Transfer-Encoding"]; + + // Check for a Content-Length field + NSString *contentLength = [request headerField:@"Content-Length"]; + + // Content-Length MUST be present for upload methods (such as POST or PUT) + // and MUST NOT be present for other methods. + BOOL expectsUpload = [self expectsRequestBodyFromMethod:method atPath:uri]; + + if (expectsUpload) + { + if (transferEncoding && ![transferEncoding caseInsensitiveCompare:@"Chunked"]) + { + requestContentLength = -1; + } + else + { + if (contentLength == nil) + { + HTTPLogWarn(@"%@[%p]: Method expects request body, but had no specified Content-Length", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + if (![NSNumber parseString:(NSString *)contentLength intoUInt64:&requestContentLength]) + { + HTTPLogWarn(@"%@[%p]: Unable to parse Content-Length header into a valid number", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + } + } + else + { + if (contentLength != nil) + { + // Received Content-Length header for method not expecting an upload. + // This better be zero... + + if (![NSNumber parseString:(NSString *)contentLength intoUInt64:&requestContentLength]) + { + HTTPLogWarn(@"%@[%p]: Unable to parse Content-Length header into a valid number", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + if (requestContentLength > 0) + { + HTTPLogWarn(@"%@[%p]: Method not expecting request body had non-zero Content-Length", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + } + + requestContentLength = 0; + requestContentLengthReceived = 0; + } + + // Check to make sure the given method is supported + if (![self supportsMethod:method atPath:uri]) + { + // The method is unsupported - either in general, or for this specific request + // Send a 405 - Method not allowed response + [self handleUnknownMethod:method]; + return; + } + + if (expectsUpload) + { + // Reset the total amount of data received for the upload + requestContentLengthReceived = 0; + + // Prepare for the upload + [self prepareForBodyWithSize:requestContentLength]; + + if (requestContentLength > 0) + { + // Start reading the request body + if (requestContentLength == -1) + { + // Chunked transfer + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_CHUNK_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_SIZE]; + } + else + { + NSUInteger bytesToRead; + if (requestContentLength < POST_CHUNKSIZE) + bytesToRead = (NSUInteger)requestContentLength; + else + bytesToRead = POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_BODY]; + } + } + else + { + // Empty upload + [self finishBody]; + [self replyToHTTPRequest]; + } + } + else + { + // Now we need to reply to the request + [self replyToHTTPRequest]; + } + } + } + else + { + BOOL doneReadingRequest = NO; + + // A chunked message body contains a series of chunks, + // followed by a line with "0" (zero), + // followed by optional footers (just like headers), + // and a blank line. + // + // Each chunk consists of two parts: + // + // 1. A line with the size of the chunk data, in hex, + // possibly followed by a semicolon and extra parameters you can ignore (none are currently standard), + // and ending with CRLF. + // 2. The data itself, followed by CRLF. + // + // Part 1 is represented by HTTP_REQUEST_CHUNK_SIZE + // Part 2 is represented by HTTP_REQUEST_CHUNK_DATA and HTTP_REQUEST_CHUNK_TRAILER + // where the trailer is the CRLF that follows the data. + // + // The optional footers and blank line are represented by HTTP_REQUEST_CHUNK_FOOTER. + + if (tag == HTTP_REQUEST_CHUNK_SIZE) + { + // We have just read in a line with the size of the chunk data, in hex, + // possibly followed by a semicolon and extra parameters that can be ignored, + // and ending with CRLF. + + NSString *sizeLine = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + errno = 0; // Reset errno before calling strtoull() to ensure it is always zero on success + requestChunkSize = (UInt64)strtoull([sizeLine UTF8String], NULL, 16); + requestChunkSizeReceived = 0; + + if (errno != 0) + { + HTTPLogWarn(@"%@[%p]: Method expects chunk size, but received something else", THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + if (requestChunkSize > 0) + { + NSUInteger bytesToRead; + bytesToRead = (requestChunkSize < POST_CHUNKSIZE) ? (NSUInteger)requestChunkSize : POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_CHUNK_DATA]; + } + else + { + // This is the "0" (zero) line, + // which is to be followed by optional footers (just like headers) and finally a blank line. + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_FOOTER]; + } + + return; + } + else if (tag == HTTP_REQUEST_CHUNK_DATA) + { + // We just read part of the actual data. + + requestContentLengthReceived += [data length]; + requestChunkSizeReceived += [data length]; + + [self processBodyData:data]; + + UInt64 bytesLeft = requestChunkSize - requestChunkSizeReceived; + if (bytesLeft > 0) + { + NSUInteger bytesToRead = (bytesLeft < POST_CHUNKSIZE) ? (NSUInteger)bytesLeft : POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_CHUNK_DATA]; + } + else + { + // We've read in all the data for this chunk. + // The data is followed by a CRLF, which we need to read (and basically ignore) + + [asyncSocket readDataToLength:2 + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_CHUNK_TRAILER]; + } + + return; + } + else if (tag == HTTP_REQUEST_CHUNK_TRAILER) + { + // This should be the CRLF following the data. + // Just ensure it's a CRLF. + + if (![data isEqualToData:[GCDAsyncSocket CRLFData]]) + { + HTTPLogWarn(@"%@[%p]: Method expects chunk trailer, but is missing", THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + // Now continue with the next chunk + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_CHUNK_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_SIZE]; + + } + else if (tag == HTTP_REQUEST_CHUNK_FOOTER) + { + if (++numHeaderLines > MAX_HEADER_LINES) + { + // Reached the maximum amount of header lines in a single HTTP request + // This could be an attempted DOS attack + [asyncSocket disconnect]; + + // Explictly return to ensure we don't do anything after the socket disconnect + return; + } + + if ([data length] > 2) + { + // We read in a footer. + // In the future we may want to append these to the request. + // For now we ignore, and continue reading the footers, waiting for the final blank line. + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_FOOTER]; + } + else + { + doneReadingRequest = YES; + } + } + else // HTTP_REQUEST_BODY + { + // Handle a chunk of data from the POST body + + requestContentLengthReceived += [data length]; + [self processBodyData:data]; + + if (requestContentLengthReceived < requestContentLength) + { + // We're not done reading the post body yet... + + UInt64 bytesLeft = requestContentLength - requestContentLengthReceived; + + NSUInteger bytesToRead = bytesLeft < POST_CHUNKSIZE ? (NSUInteger)bytesLeft : POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_BODY]; + } + else + { + doneReadingRequest = YES; + } + } + + // Now that the entire body has been received, we need to reply to the request + + if (doneReadingRequest) + { + [self finishBody]; + [self replyToHTTPRequest]; + } + } +} + +/** + * This method is called after the socket has successfully written data to the stream. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag +{ + BOOL doneSendingResponse = NO; + + if (tag == HTTP_PARTIAL_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + + // We only wrote a part of the response - there may be more + [self continueSendingStandardResponseBody]; + } + else if (tag == HTTP_CHUNKED_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue. + // This will allow asynchronous responses to continue sending more data. + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + // Don't continue sending the response yet. + // The chunked footer that was sent after the body will tell us if we have more data to send. + } + else if (tag == HTTP_CHUNKED_RESPONSE_FOOTER) + { + // Normal chunked footer indicating we have more data to send (non final footer). + [self continueSendingStandardResponseBody]; + } + else if (tag == HTTP_PARTIAL_RANGE_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + // We only wrote a part of the range - there may be more + [self continueSendingSingleRangeResponseBody]; + } + else if (tag == HTTP_PARTIAL_RANGES_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + // We only wrote part of the range - there may be more, or there may be more ranges + [self continueSendingMultiRangeResponseBody]; + } + else if (tag == HTTP_RESPONSE || tag == HTTP_FINAL_RESPONSE) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) + { + [responseDataSizes removeObjectAtIndex:0]; + } + + doneSendingResponse = YES; + } + + if (doneSendingResponse) + { + // Inform the http response that we're done + if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) + { + [httpResponse connectionDidClose]; + } + + + if (tag == HTTP_FINAL_RESPONSE) + { + // Cleanup after the last request + [self finishResponse]; + + // Terminate the connection + [asyncSocket disconnect]; + + // Explictly return to ensure we don't do anything after the socket disconnects + return; + } + else + { + if ([self shouldDie]) + { + // Cleanup after the last request + // Note: Don't do this before calling shouldDie, as it needs the request object still. + [self finishResponse]; + + // The only time we should invoke [self die] is from socketDidDisconnect, + // or if the socket gets taken over by someone else like a WebSocket. + + [asyncSocket disconnect]; + } + else + { + // Cleanup after the last request + [self finishResponse]; + + // Prepare for the next request + + // If this assertion fails, it likely means you overrode the + // finishBody method and forgot to call [super finishBody]. + NSAssert(request == nil, @"Request not properly released in finishBody"); + + request = [[HTTPMessage alloc] initEmptyRequest]; + + numHeaderLines = 0; + sentResponseHeaders = NO; + + // And start listening for more requests + [self startReadingRequest]; + } + } + } +} + +/** + * Sent after the socket has been disconnected. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + HTTPLogTrace(); + + asyncSocket = nil; + + [self die]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark HTTPResponse Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method may be called by asynchronous HTTPResponse objects. + * That is, HTTPResponse objects that return YES in their "- (BOOL)isAsynchronous" method. + * + * This informs us that the response object has generated more data that we may be able to send. +**/ +- (void)responseHasAvailableData:(NSObject *)sender +{ + HTTPLogTrace(); + + // We always dispatch this asynchronously onto our connectionQueue, + // even if the connectionQueue is the current queue. + // + // We do this to give the HTTPResponse classes the flexibility to call + // this method whenever they want, even from within a readDataOfLength method. + + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + if (sender != httpResponse) + { + HTTPLogWarn(@"%@[%p]: %@ - Sender is not current httpResponse", THIS_FILE, self, THIS_METHOD); + return; + } + + if (!sentResponseHeaders) + { + [self sendResponseHeadersAndBody]; + } + else + { + if (ranges == nil) + { + [self continueSendingStandardResponseBody]; + } + else + { + if ([ranges count] == 1) + [self continueSendingSingleRangeResponseBody]; + else + [self continueSendingMultiRangeResponseBody]; + } + } + }}); +} + +/** + * This method is called if the response encounters some critical error, + * and it will be unable to fullfill the request. +**/ +- (void)responseDidAbort:(NSObject *)sender +{ + HTTPLogTrace(); + + // We always dispatch this asynchronously onto our connectionQueue, + // even if the connectionQueue is the current queue. + // + // We do this to give the HTTPResponse classes the flexibility to call + // this method whenever they want, even from within a readDataOfLength method. + + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + if (sender != httpResponse) + { + HTTPLogWarn(@"%@[%p]: %@ - Sender is not current httpResponse", THIS_FILE, self, THIS_METHOD); + return; + } + + [asyncSocket disconnectAfterWriting]; + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Post Request +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called after each response has been fully sent. + * Since a single connection may handle multiple request/responses, this method may be called multiple times. + * That is, it will be called after completion of each response. +**/ +- (void)finishResponse +{ + HTTPLogTrace(); + + // Override me if you want to perform any custom actions after a response has been fully sent. + // This is the place to release memory or resources associated with the last request. + // + // If you override this method, you should take care to invoke [super finishResponse] at some point. + + request = nil; + + httpResponse = nil; + + ranges = nil; + ranges_headers = nil; + ranges_boundry = nil; +} + +/** + * This method is called after each successful response has been fully sent. + * It determines whether the connection should stay open and handle another request. +**/ +- (BOOL)shouldDie +{ + HTTPLogTrace(); + + // Override me if you have any need to force close the connection. + // You may do so by simply returning YES. + // + // If you override this method, you should take care to fall through with [super shouldDie] + // instead of returning NO. + + + BOOL shouldDie = NO; + + NSString *version = [request version]; + if ([version isEqualToString:HTTPVersion1_1]) + { + // HTTP version 1.1 + // Connection should only be closed if request included "Connection: close" header + + NSString *connection = [request headerField:@"Connection"]; + + shouldDie = (connection && ([connection caseInsensitiveCompare:@"close"] == NSOrderedSame)); + } + else if ([version isEqualToString:HTTPVersion1_0]) + { + // HTTP version 1.0 + // Connection should be closed unless request included "Connection: Keep-Alive" header + + NSString *connection = [request headerField:@"Connection"]; + + if (connection == nil) + shouldDie = YES; + else + shouldDie = [connection caseInsensitiveCompare:@"Keep-Alive"] != NSOrderedSame; + } + + return shouldDie; +} + +- (void)die +{ + HTTPLogTrace(); + + // Override me if you want to perform any custom actions when a connection is closed. + // Then call [super die] when you're done. + // + // See also the finishResponse method. + // + // Important: There is a rare timing condition where this method might get invoked twice. + // If you override this method, you should be prepared for this situation. + + // Inform the http response that we're done + if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) + { + [httpResponse connectionDidClose]; + } + + // Release the http response so we don't call it's connectionDidClose method again in our dealloc method + httpResponse = nil; + + // Post notification of dead connection + // This will allow our server to release us from its array of connections + [[NSNotificationCenter defaultCenter] postNotificationName:HTTPConnectionDidDieNotification object:self]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation HTTPConfig + +@synthesize server; +@synthesize documentRoot; +@synthesize queue; + +- (id)initWithServer:(HTTPServer *)aServer documentRoot:(NSString *)aDocumentRoot +{ + if ((self = [super init])) + { + server = aServer; + documentRoot = aDocumentRoot; + } + return self; +} + +- (id)initWithServer:(HTTPServer *)aServer documentRoot:(NSString *)aDocumentRoot queue:(dispatch_queue_t)q +{ + if ((self = [super init])) + { + server = aServer; + + documentRoot = [aDocumentRoot stringByStandardizingPath]; + if ([documentRoot hasSuffix:@"/"]) + { + documentRoot = [documentRoot stringByAppendingString:@"/"]; + } + + if (q) + { + queue = q; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(queue); + #endif + } + } + return self; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (queue) dispatch_release(queue); + #endif +} + +@end diff --git a/msext/Class/http/Core/HTTPLogging.h b/msext/Class/http/Core/HTTPLogging.h new file mode 100755 index 0000000..2e4bbcd --- /dev/null +++ b/msext/Class/http/Core/HTTPLogging.h @@ -0,0 +1,136 @@ +/** + * In order to provide fast and flexible logging, this project uses Cocoa Lumberjack. + * + * The Google Code page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * Here's what you need to know concerning how logging is setup for CocoaHTTPServer: + * + * There are 4 log levels: + * - Error + * - Warning + * - Info + * - Verbose + * + * In addition to this, there is a Trace flag that can be enabled. + * When tracing is enabled, it spits out the methods that are being called. + * + * Please note that tracing is separate from the log levels. + * For example, one could set the log level to warning, and enable tracing. + * + * All logging is asynchronous, except errors. + * To use logging within your own custom files, follow the steps below. + * + * Step 1: + * Import this header in your implementation file: + * + * #import "HTTPLogging.h" + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const int httpLogLevel = HTTP_LOG_LEVEL_VERBOSE; + * + * If you wish to enable tracing, you could do something like this: + * + * // Debug levels: off, error, warn, info, verbose + * static const int httpLogLevel = HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_TRACE; + * + * Step 3: + * Replace your NSLog statements with HTTPLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> HTTPLogError(@"Fatal error, no dohickey found!"); + * + * HTTPLog works exactly the same as NSLog. + * This means you can pass it multiple variables just like NSLog. +**/ + +#import "DDLog.h" + +// Define logging context for every log message coming from the HTTP server. +// The logging context can be extracted from the DDLogMessage from within the logging framework, +// which gives loggers, formatters, and filters the ability to optionally process them differently. + +#define HTTP_LOG_CONTEXT 80 + +// Configure log levels. + +#define HTTP_LOG_FLAG_ERROR (1 << 0) // 0...00001 +#define HTTP_LOG_FLAG_WARN (1 << 1) // 0...00010 +#define HTTP_LOG_FLAG_INFO (1 << 2) // 0...00100 +#define HTTP_LOG_FLAG_VERBOSE (1 << 3) // 0...01000 + +#define HTTP_LOG_LEVEL_OFF 0 // 0...00000 +#define HTTP_LOG_LEVEL_ERROR (HTTP_LOG_LEVEL_OFF | HTTP_LOG_FLAG_ERROR) // 0...00001 +#define HTTP_LOG_LEVEL_WARN (HTTP_LOG_LEVEL_ERROR | HTTP_LOG_FLAG_WARN) // 0...00011 +#define HTTP_LOG_LEVEL_INFO (HTTP_LOG_LEVEL_WARN | HTTP_LOG_FLAG_INFO) // 0...00111 +#define HTTP_LOG_LEVEL_VERBOSE (HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_VERBOSE) // 0...01111 + +// Setup fine grained logging. +// The first 4 bits are being used by the standard log levels (0 - 3) +// +// We're going to add tracing, but NOT as a log level. +// Tracing can be turned on and off independently of log level. + +#define HTTP_LOG_FLAG_TRACE (1 << 4) // 0...10000 + +// Setup the usual boolean macros. + +#define HTTP_LOG_ERROR (httpLogLevel & HTTP_LOG_FLAG_ERROR) +#define HTTP_LOG_WARN (httpLogLevel & HTTP_LOG_FLAG_WARN) +#define HTTP_LOG_INFO (httpLogLevel & HTTP_LOG_FLAG_INFO) +#define HTTP_LOG_VERBOSE (httpLogLevel & HTTP_LOG_FLAG_VERBOSE) +#define HTTP_LOG_TRACE (httpLogLevel & HTTP_LOG_FLAG_TRACE) + +// Configure asynchronous logging. +// We follow the default configuration, +// but we reserve a special macro to easily disable asynchronous logging for debugging purposes. + +#define HTTP_LOG_ASYNC_ENABLED YES + +#define HTTP_LOG_ASYNC_ERROR ( NO && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_WARN (YES && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_INFO (YES && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_VERBOSE (YES && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_TRACE (YES && HTTP_LOG_ASYNC_ENABLED) + +// Define logging primitives. + +#define HTTPLogError(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_ERROR, httpLogLevel, HTTP_LOG_FLAG_ERROR, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogWarn(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_WARN, httpLogLevel, HTTP_LOG_FLAG_WARN, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogInfo(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_INFO, httpLogLevel, HTTP_LOG_FLAG_INFO, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogVerbose(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_VERBOSE, httpLogLevel, HTTP_LOG_FLAG_VERBOSE, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogTrace() LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \ + HTTP_LOG_CONTEXT, @"%@[%p]: %@", THIS_FILE, self, THIS_METHOD) + +#define HTTPLogTrace2(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + + +#define HTTPLogCError(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_ERROR, httpLogLevel, HTTP_LOG_FLAG_ERROR, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogCWarn(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_WARN, httpLogLevel, HTTP_LOG_FLAG_WARN, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogCInfo(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_INFO, httpLogLevel, HTTP_LOG_FLAG_INFO, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogCVerbose(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_VERBOSE, httpLogLevel, HTTP_LOG_FLAG_VERBOSE, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define HTTPLogCTrace() LOG_C_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \ + HTTP_LOG_CONTEXT, @"%@[%p]: %@", THIS_FILE, self, __FUNCTION__) + +#define HTTPLogCTrace2(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \ + HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + diff --git a/msext/Class/http/Core/HTTPMessage.h b/msext/Class/http/Core/HTTPMessage.h new file mode 100755 index 0000000..20ea921 --- /dev/null +++ b/msext/Class/http/Core/HTTPMessage.h @@ -0,0 +1,48 @@ +/** + * The HTTPMessage class is a simple Objective-C wrapper around Apple's CFHTTPMessage class. +**/ + +#import + +#if TARGET_OS_IPHONE + // Note: You may need to add the CFNetwork Framework to your project + #import +#endif + +#define HTTPVersion1_0 ((NSString *)kCFHTTPVersion1_0) +#define HTTPVersion1_1 ((NSString *)kCFHTTPVersion1_1) + + +@interface HTTPMessage : NSObject +{ + CFHTTPMessageRef message; +} + +- (id)initEmptyRequest; + +- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version; + +- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version; + +- (BOOL)appendData:(NSData *)data; + +- (BOOL)isHeaderComplete; + +- (NSString *)version; + +- (NSString *)method; +- (NSURL *)url; + +- (NSInteger)statusCode; + +- (NSDictionary *)allHeaderFields; +- (NSString *)headerField:(NSString *)headerField; + +- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue; + +- (NSData *)messageData; + +- (NSData *)body; +- (void)setBody:(NSData *)body; + +@end diff --git a/msext/Class/http/Core/HTTPMessage.m b/msext/Class/http/Core/HTTPMessage.m new file mode 100755 index 0000000..e2c7e00 --- /dev/null +++ b/msext/Class/http/Core/HTTPMessage.m @@ -0,0 +1,113 @@ +#import "HTTPMessage.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation HTTPMessage + +- (id)initEmptyRequest +{ + if ((self = [super init])) + { + message = CFHTTPMessageCreateEmpty(NULL, YES); + } + return self; +} + +- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version +{ + if ((self = [super init])) + { + message = CFHTTPMessageCreateRequest(NULL, + (__bridge CFStringRef)method, + (__bridge CFURLRef)url, + (__bridge CFStringRef)version); + } + return self; +} + +- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version +{ + if ((self = [super init])) + { + message = CFHTTPMessageCreateResponse(NULL, + (CFIndex)code, + (__bridge CFStringRef)description, + (__bridge CFStringRef)version); + } + return self; +} + +- (void)dealloc +{ + if (message) + { + CFRelease(message); + } +} + +- (BOOL)appendData:(NSData *)data +{ + return CFHTTPMessageAppendBytes(message, [data bytes], [data length]); +} + +- (BOOL)isHeaderComplete +{ + return CFHTTPMessageIsHeaderComplete(message); +} + +- (NSString *)version +{ + return (__bridge_transfer NSString *)CFHTTPMessageCopyVersion(message); +} + +- (NSString *)method +{ + return (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod(message); +} + +- (NSURL *)url +{ + return (__bridge_transfer NSURL *)CFHTTPMessageCopyRequestURL(message); +} + +- (NSInteger)statusCode +{ + return (NSInteger)CFHTTPMessageGetResponseStatusCode(message); +} + +- (NSDictionary *)allHeaderFields +{ + return (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message); +} + +- (NSString *)headerField:(NSString *)headerField +{ + return (__bridge_transfer NSString *)CFHTTPMessageCopyHeaderFieldValue(message, (__bridge CFStringRef)headerField); +} + +- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue +{ + CFHTTPMessageSetHeaderFieldValue(message, + (__bridge CFStringRef)headerField, + (__bridge CFStringRef)headerFieldValue); +} + +- (NSData *)messageData +{ + return (__bridge_transfer NSData *)CFHTTPMessageCopySerializedMessage(message); +} + +- (NSData *)body +{ + return (__bridge_transfer NSData *)CFHTTPMessageCopyBody(message); +} + +- (void)setBody:(NSData *)body +{ + CFHTTPMessageSetBody(message, (__bridge CFDataRef)body); +} + +@end diff --git a/msext/Class/http/Core/HTTPResponse.h b/msext/Class/http/Core/HTTPResponse.h new file mode 100755 index 0000000..f303cf3 --- /dev/null +++ b/msext/Class/http/Core/HTTPResponse.h @@ -0,0 +1,149 @@ +#import + + +@protocol HTTPResponse + +/** + * Returns the length of the data in bytes. + * If you don't know the length in advance, implement the isChunked method and have it return YES. +**/ +- (UInt64)contentLength; + +/** + * The HTTP server supports range requests in order to allow things like + * file download resumption and optimized streaming on mobile devices. +**/ +- (UInt64)offset; +- (void)setOffset:(UInt64)offset; + +/** + * Returns the data for the response. + * You do not have to return data of the exact length that is given. + * You may optionally return data of a lesser length. + * However, you must never return data of a greater length than requested. + * Doing so could disrupt proper support for range requests. + * + * To support asynchronous responses, read the discussion at the bottom of this header. +**/ +- (NSData *)readDataOfLength:(NSUInteger)length; + +/** + * Should only return YES after the HTTPConnection has read all available data. + * That is, all data for the response has been returned to the HTTPConnection via the readDataOfLength method. +**/ +- (BOOL)isDone; + +@optional + +/** + * If you need time to calculate any part of the HTTP response headers (status code or header fields), + * this method allows you to delay sending the headers so that you may asynchronously execute the calculations. + * Simply implement this method and return YES until you have everything you need concerning the headers. + * + * This method ties into the asynchronous response architecture of the HTTPConnection. + * You should read the full discussion at the bottom of this header. + * + * If you return YES from this method, + * the HTTPConnection will wait for you to invoke the responseHasAvailableData method. + * After you do, the HTTPConnection will again invoke this method to see if the response is ready to send the headers. + * + * You should only delay sending the headers until you have everything you need concerning just the headers. + * Asynchronously generating the body of the response is not an excuse to delay sending the headers. + * Instead you should tie into the asynchronous response architecture, and use techniques such as the isChunked method. + * + * Important: You should read the discussion at the bottom of this header. +**/ +- (BOOL)delayResponseHeaders; + +/** + * Status code for response. + * Allows for responses such as redirect (301), etc. +**/ +- (NSInteger)status; + +/** + * If you want to add any extra HTTP headers to the response, + * simply return them in a dictionary in this method. +**/ +- (NSDictionary *)httpHeaders; + +/** + * If you don't know the content-length in advance, + * implement this method in your custom response class and return YES. + * + * Important: You should read the discussion at the bottom of this header. +**/ +- (BOOL)isChunked; + +/** + * This method is called from the HTTPConnection class when the connection is closed, + * or when the connection is finished with the response. + * If your response is asynchronous, you should implement this method so you know not to + * invoke any methods on the HTTPConnection after this method is called (as the connection may be deallocated). +**/ +- (void)connectionDidClose; + +@end + + +/** + * Important notice to those implementing custom asynchronous and/or chunked responses: + * + * HTTPConnection supports asynchronous responses. All you have to do in your custom response class is + * asynchronously generate the response, and invoke HTTPConnection's responseHasAvailableData method. + * You don't have to wait until you have all of the response ready to invoke this method. For example, if you + * generate the response in incremental chunks, you could call responseHasAvailableData after generating + * each chunk. Please see the HTTPAsyncFileResponse class for an example of how to do this. + * + * The normal flow of events for an HTTPConnection while responding to a request is like this: + * - Send http resopnse headers + * - Get data from response via readDataOfLength method. + * - Add data to asyncSocket's write queue. + * - Wait for asyncSocket to notify it that the data has been sent. + * - Get more data from response via readDataOfLength method. + * - ... continue this cycle until the entire response has been sent. + * + * With an asynchronous response, the flow is a little different. + * + * First the HTTPResponse is given the opportunity to postpone sending the HTTP response headers. + * This allows the response to asynchronously execute any code needed to calculate a part of the header. + * An example might be the response needs to generate some custom header fields, + * or perhaps the response needs to look for a resource on network-attached storage. + * Since the network-attached storage may be slow, the response doesn't know whether to send a 200 or 404 yet. + * In situations such as this, the HTTPResponse simply implements the delayResponseHeaders method and returns YES. + * After returning YES from this method, the HTTPConnection will wait until the response invokes its + * responseHasAvailableData method. After this occurs, the HTTPConnection will again query the delayResponseHeaders + * method to see if the response is ready to send the headers. + * This cycle will continue until the delayResponseHeaders method returns NO. + * + * You should only delay sending the response headers until you have everything you need concerning just the headers. + * Asynchronously generating the body of the response is not an excuse to delay sending the headers. + * + * After the response headers have been sent, the HTTPConnection calls your readDataOfLength method. + * You may or may not have any available data at this point. If you don't, then simply return nil. + * You should later invoke HTTPConnection's responseHasAvailableData when you have data to send. + * + * You don't have to keep track of when you return nil in the readDataOfLength method, or how many times you've invoked + * responseHasAvailableData. Just simply call responseHasAvailableData whenever you've generated new data, and + * return nil in your readDataOfLength whenever you don't have any available data in the requested range. + * HTTPConnection will automatically detect when it should be requesting new data and will act appropriately. + * + * It's important that you also keep in mind that the HTTP server supports range requests. + * The setOffset method is mandatory, and should not be ignored. + * Make sure you take into account the offset within the readDataOfLength method. + * You should also be aware that the HTTPConnection automatically sorts any range requests. + * So if your setOffset method is called with a value of 100, then you can safely release bytes 0-99. + * + * HTTPConnection can also help you keep your memory footprint small. + * Imagine you're dynamically generating a 10 MB response. You probably don't want to load all this data into + * RAM, and sit around waiting for HTTPConnection to slowly send it out over the network. All you need to do + * is pay attention to when HTTPConnection requests more data via readDataOfLength. This is because HTTPConnection + * will never allow asyncSocket's write queue to get much bigger than READ_CHUNKSIZE bytes. You should + * consider how you might be able to take advantage of this fact to generate your asynchronous response on demand, + * while at the same time keeping your memory footprint small, and your application lightning fast. + * + * If you don't know the content-length in advanced, you should also implement the isChunked method. + * This means the response will not include a Content-Length header, and will instead use "Transfer-Encoding: chunked". + * There's a good chance that if your response is asynchronous and dynamic, it's also chunked. + * If your response is chunked, you don't need to worry about range requests. +**/ diff --git a/msext/Class/http/Core/HTTPServer.h b/msext/Class/http/Core/HTTPServer.h new file mode 100755 index 0000000..1d37cb6 --- /dev/null +++ b/msext/Class/http/Core/HTTPServer.h @@ -0,0 +1,205 @@ +#import + +@class GCDAsyncSocket; +@class WebSocket; + +#if TARGET_OS_IPHONE + #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 // iPhone 4.0 + #define IMPLEMENTED_PROTOCOLS + #else + #define IMPLEMENTED_PROTOCOLS + #endif +#else + #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060 // Mac OS X 10.6 + #define IMPLEMENTED_PROTOCOLS + #else + #define IMPLEMENTED_PROTOCOLS + #endif +#endif + + +@interface HTTPServer : NSObject IMPLEMENTED_PROTOCOLS +{ + // Underlying asynchronous TCP/IP socket + GCDAsyncSocket *asyncSocket; + + // Dispatch queues + dispatch_queue_t serverQueue; + dispatch_queue_t connectionQueue; + void *IsOnServerQueueKey; + void *IsOnConnectionQueueKey; + + // HTTP server configuration + NSString *documentRoot; + Class connectionClass; + NSString *interface; + UInt16 port; + + // NSNetService and related variables + NSNetService *netService; + NSString *domain; + NSString *type; + NSString *name; + NSString *publishedName; + NSDictionary *txtRecordDictionary; + + // Connection management + NSMutableArray *connections; + NSMutableArray *webSockets; + NSLock *connectionsLock; + NSLock *webSocketsLock; + + BOOL isRunning; +} + +/** + * Specifies the document root to serve files from. + * For example, if you set this to "/Users//Sites", + * then it will serve files out of the local Sites directory (including subdirectories). + * + * The default value is nil. + * The default server configuration will not serve any files until this is set. + * + * If you change the documentRoot while the server is running, + * the change will affect future incoming http connections. +**/ +- (NSString *)documentRoot; +- (void)setDocumentRoot:(NSString *)value; + +/** + * The connection class is the class used to handle incoming HTTP connections. + * + * The default value is [HTTPConnection class]. + * You can override HTTPConnection, and then set this to [MyHTTPConnection class]. + * + * If you change the connectionClass while the server is running, + * the change will affect future incoming http connections. +**/ +- (Class)connectionClass; +- (void)setConnectionClass:(Class)value; + +/** + * Set what interface you'd like the server to listen on. + * By default this is nil, which causes the server to listen on all available interfaces like en1, wifi etc. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. +**/ +- (NSString *)interface; +- (void)setInterface:(NSString *)value; + +/** + * The port number to run the HTTP server on. + * + * The default port number is zero, meaning the server will automatically use any available port. + * This is the recommended port value, as it avoids possible port conflicts with other applications. + * Technologies such as Bonjour can be used to allow other applications to automatically discover the port number. + * + * Note: As is common on most OS's, you need root privledges to bind to port numbers below 1024. + * + * You can change the port property while the server is running, but it won't affect the running server. + * To actually change the port the server is listening for connections on you'll need to restart the server. + * + * The listeningPort method will always return the port number the running server is listening for connections on. + * If the server is not running this method returns 0. +**/ +- (UInt16)port; +- (UInt16)listeningPort; +- (void)setPort:(UInt16)value; + +/** + * Bonjour domain for publishing the service. + * The default value is "local.". + * + * Note: Bonjour publishing requires you set a type. + * + * If you change the domain property after the bonjour service has already been published (server already started), + * you'll need to invoke the republishBonjour method to update the broadcasted bonjour service. +**/ +- (NSString *)domain; +- (void)setDomain:(NSString *)value; + +/** + * Bonjour name for publishing the service. + * The default value is "". + * + * If using an empty string ("") for the service name when registering, + * the system will automatically use the "Computer Name". + * Using an empty string will also handle name conflicts + * by automatically appending a digit to the end of the name. + * + * Note: Bonjour publishing requires you set a type. + * + * If you change the name after the bonjour service has already been published (server already started), + * you'll need to invoke the republishBonjour method to update the broadcasted bonjour service. + * + * The publishedName method will always return the actual name that was published via the bonjour service. + * If the service is not running this method returns nil. +**/ +- (NSString *)name; +- (NSString *)publishedName; +- (void)setName:(NSString *)value; + +/** + * Bonjour type for publishing the service. + * The default value is nil. + * The service will not be published via bonjour unless the type is set. + * + * If you wish to publish the service as a traditional HTTP server, you should set the type to be "_http._tcp.". + * + * If you change the type after the bonjour service has already been published (server already started), + * you'll need to invoke the republishBonjour method to update the broadcasted bonjour service. +**/ +- (NSString *)type; +- (void)setType:(NSString *)value; + +/** + * Republishes the service via bonjour if the server is running. + * If the service was not previously published, this method will publish it (if the server is running). +**/ +- (void)republishBonjour; + +/** + * +**/ +- (NSDictionary *)TXTRecordDictionary; +- (void)setTXTRecordDictionary:(NSDictionary *)dict; + +/** + * Attempts to starts the server on the configured port, interface, etc. + * + * If an error occurs, this method returns NO and sets the errPtr (if given). + * Otherwise returns YES on success. + * + * Some examples of errors that might occur: + * - You specified the server listen on a port which is already in use by another application. + * - You specified the server listen on a port number below 1024, which requires root priviledges. + * + * Code Example: + * + * NSError *err = nil; + * if (![httpServer start:&err]) + * { + * NSLog(@"Error starting http server: %@", err); + * } +**/ +- (BOOL)start:(NSError **)errPtr; + +/** + * Stops the server, preventing it from accepting any new connections. + * You may specify whether or not you want to close the existing client connections. + * + * The default stop method (with no arguments) will close any existing connections. (It invokes [self stop:NO]) +**/ +- (void)stop; +- (void)stop:(BOOL)keepExistingConnections; + +- (BOOL)isRunning; + +- (void)addWebSocket:(WebSocket *)ws; + +- (NSUInteger)numberOfHTTPConnections; +- (NSUInteger)numberOfWebSocketConnections; + +@end diff --git a/msext/Class/http/Core/HTTPServer.m b/msext/Class/http/Core/HTTPServer.m new file mode 100755 index 0000000..57384f7 --- /dev/null +++ b/msext/Class/http/Core/HTTPServer.m @@ -0,0 +1,772 @@ +#import "HTTPServer.h" +#import "GCDAsyncSocket.h" +#import "HTTPConnection.h" +#import "WebSocket.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_INFO; // | HTTP_LOG_FLAG_TRACE; + +@interface HTTPServer (PrivateAPI) + +- (void)unpublishBonjour; +- (void)publishBonjour; + ++ (void)startBonjourThreadIfNeeded; ++ (void)performBonjourBlock:(dispatch_block_t)block; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation HTTPServer + +/** + * Standard Constructor. + * Instantiates an HTTP server, but does not start it. +**/ +- (id)init +{ + if ((self = [super init])) + { + HTTPLogTrace(); + + // Setup underlying dispatch queues + serverQueue = dispatch_queue_create("HTTPServer", NULL); + connectionQueue = dispatch_queue_create("HTTPConnection", NULL); + + IsOnServerQueueKey = &IsOnServerQueueKey; + IsOnConnectionQueueKey = &IsOnConnectionQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; // Whatever, just not null + + dispatch_queue_set_specific(serverQueue, IsOnServerQueueKey, nonNullUnusedPointer, NULL); + dispatch_queue_set_specific(connectionQueue, IsOnConnectionQueueKey, nonNullUnusedPointer, NULL); + + // Initialize underlying GCD based tcp socket + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:serverQueue]; + + // Use default connection class of HTTPConnection + connectionClass = [HTTPConnection self]; + + // By default bind on all available interfaces, en1, wifi etc + interface = nil; + + // Use a default port of 0 + // This will allow the kernel to automatically pick an open port for us + port = 0; + + // Configure default values for bonjour service + + // Bonjour domain. Use the local domain by default + domain = @"local."; + + // If using an empty string ("") for the service name when registering, + // the system will automatically use the "Computer Name". + // Passing in an empty string will also handle name conflicts + // by automatically appending a digit to the end of the name. + name = @""; + + // Initialize arrays to hold all the HTTP and webSocket connections + connections = [[NSMutableArray alloc] init]; + webSockets = [[NSMutableArray alloc] init]; + + connectionsLock = [[NSLock alloc] init]; + webSocketsLock = [[NSLock alloc] init]; + + // Register for notifications of closed connections + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(connectionDidDie:) + name:HTTPConnectionDidDieNotification + object:nil]; + + // Register for notifications of closed websocket connections + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(webSocketDidDie:) + name:WebSocketDidDieNotification + object:nil]; + + isRunning = NO; + } + return self; +} + +/** + * Standard Deconstructor. + * Stops the server, and clients, and releases any resources connected with this instance. +**/ +- (void)dealloc +{ + HTTPLogTrace(); + + // Remove notification observer + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // Stop the server if it's running + [self stop]; + + // Release all instance variables + + #if !OS_OBJECT_USE_OBJC + dispatch_release(serverQueue); + dispatch_release(connectionQueue); + #endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The document root is filesystem root for the webserver. + * Thus requests for /index.html will be referencing the index.html file within the document root directory. + * All file requests are relative to this document root. +**/ +- (NSString *)documentRoot +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = documentRoot; + }); + + return result; +} + +- (void)setDocumentRoot:(NSString *)value +{ + HTTPLogTrace(); + + // Document root used to be of type NSURL. + // Add type checking for early warning to developers upgrading from older versions. + + if (value && ![value isKindOfClass:[NSString class]]) + { + HTTPLogWarn(@"%@: %@ - Expecting NSString parameter, received %@ parameter", + THIS_FILE, THIS_METHOD, NSStringFromClass([value class])); + return; + } + + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + documentRoot = valueCopy; + }); + +} + +/** + * The connection class is the class that will be used to handle connections. + * That is, when a new connection is created, an instance of this class will be intialized. + * The default connection class is HTTPConnection. + * If you use a different connection class, it is assumed that the class extends HTTPConnection +**/ +- (Class)connectionClass +{ + __block Class result; + + dispatch_sync(serverQueue, ^{ + result = connectionClass; + }); + + return result; +} + +- (void)setConnectionClass:(Class)value +{ + HTTPLogTrace(); + + dispatch_async(serverQueue, ^{ + connectionClass = value; + }); +} + +/** + * What interface to bind the listening socket to. +**/ +- (NSString *)interface +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = interface; + }); + + return result; +} + +- (void)setInterface:(NSString *)value +{ + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + interface = valueCopy; + }); + +} + +/** + * The port to listen for connections on. + * By default this port is initially set to zero, which allows the kernel to pick an available port for us. + * After the HTTP server has started, the port being used may be obtained by this method. +**/ +- (UInt16)port +{ + __block UInt16 result; + + dispatch_sync(serverQueue, ^{ + result = port; + }); + + return result; +} + +- (UInt16)listeningPort +{ + __block UInt16 result; + + dispatch_sync(serverQueue, ^{ + if (isRunning) + result = [asyncSocket localPort]; + else + result = 0; + }); + + return result; +} + +- (void)setPort:(UInt16)value +{ + HTTPLogTrace(); + + dispatch_async(serverQueue, ^{ + port = value; + }); +} + +/** + * Domain on which to broadcast this service via Bonjour. + * The default domain is @"local". +**/ +- (NSString *)domain +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = domain; + }); + + return result; +} + +- (void)setDomain:(NSString *)value +{ + HTTPLogTrace(); + + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + domain = valueCopy; + }); + +} + +/** + * The name to use for this service via Bonjour. + * The default name is an empty string, + * which should result in the published name being the host name of the computer. +**/ +- (NSString *)name +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = name; + }); + + return result; +} + +- (NSString *)publishedName +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + + if (netService == nil) + { + result = nil; + } + else + { + + dispatch_block_t bonjourBlock = ^{ + result = [[netService name] copy]; + }; + + [[self class] performBonjourBlock:bonjourBlock]; + } + }); + + return result; +} + +- (void)setName:(NSString *)value +{ + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + name = valueCopy; + }); + +} + +/** + * The type of service to publish via Bonjour. + * No type is set by default, and one must be set in order for the service to be published. +**/ +- (NSString *)type +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = type; + }); + + return result; +} + +- (void)setType:(NSString *)value +{ + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + type = valueCopy; + }); + +} + +/** + * The extra data to use for this service via Bonjour. +**/ +- (NSDictionary *)TXTRecordDictionary +{ + __block NSDictionary *result; + + dispatch_sync(serverQueue, ^{ + result = txtRecordDictionary; + }); + + return result; +} + +- (void)setTXTRecordDictionary:(NSDictionary *)value +{ + HTTPLogTrace(); + + NSDictionary *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + + txtRecordDictionary = valueCopy; + + // Update the txtRecord of the netService if it has already been published + if (netService) + { + NSNetService *theNetService = netService; + NSData *txtRecordData = nil; + if (txtRecordDictionary) + txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary]; + + dispatch_block_t bonjourBlock = ^{ + [theNetService setTXTRecordData:txtRecordData]; + }; + + [[self class] performBonjourBlock:bonjourBlock]; + } + }); + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Control +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)start:(NSError **)errPtr +{ + HTTPLogTrace(); + + __block BOOL success = YES; + __block NSError *err = nil; + + dispatch_sync(serverQueue, ^{ @autoreleasepool { + + success = [asyncSocket acceptOnInterface:interface port:port error:&err]; + if (success) + { + HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]); + + isRunning = YES; + [self publishBonjour]; + } + else + { + HTTPLogError(@"%@: Failed to start HTTP Server: %@", THIS_FILE, err); + } + }}); + + if (errPtr) + *errPtr = err; + + return success; +} + +- (void)stop +{ + [self stop:NO]; +} + +- (void)stop:(BOOL)keepExistingConnections +{ + HTTPLogTrace(); + + dispatch_sync(serverQueue, ^{ @autoreleasepool { + + // First stop publishing the service via bonjour + [self unpublishBonjour]; + + // Stop listening / accepting incoming connections + [asyncSocket disconnect]; + isRunning = NO; + + if (!keepExistingConnections) + { + // Stop all HTTP connections the server owns + [connectionsLock lock]; + for (HTTPConnection *connection in connections) + { + [connection stop]; + } + [connections removeAllObjects]; + [connectionsLock unlock]; + + // Stop all WebSocket connections the server owns + [webSocketsLock lock]; + for (WebSocket *webSocket in webSockets) + { + [webSocket stop]; + } + [webSockets removeAllObjects]; + [webSocketsLock unlock]; + } + }}); +} + +- (BOOL)isRunning +{ + __block BOOL result; + + dispatch_sync(serverQueue, ^{ + result = isRunning; + }); + + return result; +} + +- (void)addWebSocket:(WebSocket *)ws +{ + [webSocketsLock lock]; + + HTTPLogTrace(); + [webSockets addObject:ws]; + + [webSocketsLock unlock]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Status +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the number of http client connections that are currently connected to the server. +**/ +- (NSUInteger)numberOfHTTPConnections +{ + NSUInteger result = 0; + + [connectionsLock lock]; + result = [connections count]; + [connectionsLock unlock]; + + return result; +} + +/** + * Returns the number of websocket client connections that are currently connected to the server. +**/ +- (NSUInteger)numberOfWebSocketConnections +{ + NSUInteger result = 0; + + [webSocketsLock lock]; + result = [webSockets count]; + [webSocketsLock unlock]; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Incoming Connections +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (HTTPConfig *)config +{ + // Override me if you want to provide a custom config to the new connection. + // + // Generally this involves overriding the HTTPConfig class to include any custom settings, + // and then having this method return an instance of 'MyHTTPConfig'. + + // Note: Think you can make the server faster by putting each connection on its own queue? + // Then benchmark it before and after and discover for yourself the shocking truth! + // + // Try the apache benchmark tool (already installed on your Mac): + // $ ab -n 1000 -c 1 http://localhost:/some_path.html + + return [[HTTPConfig alloc] initWithServer:self documentRoot:documentRoot queue:connectionQueue]; +} + +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket +{ + HTTPConnection *newConnection = (HTTPConnection *)[[connectionClass alloc] initWithAsyncSocket:newSocket + configuration:[self config]]; + [connectionsLock lock]; + [connections addObject:newConnection]; + [connectionsLock unlock]; + + [newConnection start]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Bonjour +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)publishBonjour +{ + HTTPLogTrace(); + + NSAssert(dispatch_get_specific(IsOnServerQueueKey) != NULL, @"Must be on serverQueue"); + + if (type) + { + netService = [[NSNetService alloc] initWithDomain:domain type:type name:name port:[asyncSocket localPort]]; + [netService setDelegate:self]; + + NSNetService *theNetService = netService; + NSData *txtRecordData = nil; + if (txtRecordDictionary) + txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary]; + + dispatch_block_t bonjourBlock = ^{ + + [theNetService removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + [theNetService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + [theNetService publish]; + + // Do not set the txtRecordDictionary prior to publishing!!! + // This will cause the OS to crash!!! + if (txtRecordData) + { + [theNetService setTXTRecordData:txtRecordData]; + } + }; + + [[self class] startBonjourThreadIfNeeded]; + [[self class] performBonjourBlock:bonjourBlock]; + } +} + +- (void)unpublishBonjour +{ + HTTPLogTrace(); + + NSAssert(dispatch_get_specific(IsOnServerQueueKey) != NULL, @"Must be on serverQueue"); + + if (netService) + { + NSNetService *theNetService = netService; + + dispatch_block_t bonjourBlock = ^{ + + [theNetService stop]; + }; + + [[self class] performBonjourBlock:bonjourBlock]; + + netService = nil; + } +} + +/** + * Republishes the service via bonjour if the server is running. + * If the service was not previously published, this method will publish it (if the server is running). +**/ +- (void)republishBonjour +{ + HTTPLogTrace(); + + dispatch_async(serverQueue, ^{ + + [self unpublishBonjour]; + [self publishBonjour]; + }); +} + +/** + * Called when our bonjour service has been successfully published. + * This method does nothing but output a log message telling us about the published service. +**/ +- (void)netServiceDidPublish:(NSNetService *)ns +{ + // Override me to do something here... + // + // Note: This method is invoked on our bonjour thread. + + HTTPLogInfo(@"Bonjour Service Published: domain(%@) type(%@) name(%@)", [ns domain], [ns type], [ns name]); +} + +/** + * Called if our bonjour service failed to publish itself. + * This method does nothing but output a log message telling us about the published service. +**/ +- (void)netService:(NSNetService *)ns didNotPublish:(NSDictionary *)errorDict +{ + // Override me to do something here... + // + // Note: This method in invoked on our bonjour thread. + + HTTPLogWarn(@"Failed to Publish Service: domain(%@) type(%@) name(%@) - %@", + [ns domain], [ns type], [ns name], errorDict); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is automatically called when a notification of type HTTPConnectionDidDieNotification is posted. + * It allows us to remove the connection from our array. +**/ +- (void)connectionDidDie:(NSNotification *)notification +{ + // Note: This method is called on the connection queue that posted the notification + + [connectionsLock lock]; + + HTTPLogTrace(); + [connections removeObject:[notification object]]; + + [connectionsLock unlock]; +} + +/** + * This method is automatically called when a notification of type WebSocketDidDieNotification is posted. + * It allows us to remove the websocket from our array. +**/ +- (void)webSocketDidDie:(NSNotification *)notification +{ + // Note: This method is called on the connection queue that posted the notification + + [webSocketsLock lock]; + + HTTPLogTrace(); + [webSockets removeObject:[notification object]]; + + [webSocketsLock unlock]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Bonjour Thread +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * NSNetService is runloop based, so it requires a thread with a runloop. + * This gives us two options: + * + * - Use the main thread + * - Setup our own dedicated thread + * + * Since we have various blocks of code that need to synchronously access the netservice objects, + * using the main thread becomes troublesome and a potential for deadlock. +**/ + +static NSThread *bonjourThread; + ++ (void)startBonjourThreadIfNeeded +{ + HTTPLogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + HTTPLogVerbose(@"%@: Starting bonjour thread...", THIS_FILE); + + bonjourThread = [[NSThread alloc] initWithTarget:self + selector:@selector(bonjourThread) + object:nil]; + [bonjourThread start]; + }); +} + ++ (void)bonjourThread +{ + @autoreleasepool { + + HTTPLogVerbose(@"%@: BonjourThread: Started", THIS_FILE); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for 10,000 years. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(donothingatall:) + userInfo:nil + repeats:YES]; +#pragma clang diagnostic pop + + [[NSRunLoop currentRunLoop] run]; + + HTTPLogVerbose(@"%@: BonjourThread: Aborted", THIS_FILE); + + } +} + ++ (void)executeBonjourBlock:(dispatch_block_t)block +{ + HTTPLogTrace(); + + NSAssert([NSThread currentThread] == bonjourThread, @"Executed on incorrect thread"); + + block(); +} + ++ (void)performBonjourBlock:(dispatch_block_t)block +{ + HTTPLogTrace(); + + [self performSelector:@selector(executeBonjourBlock:) + onThread:bonjourThread + withObject:block + waitUntilDone:YES]; +} + +@end diff --git a/msext/Class/http/Core/Mime/MultipartFormDataParser.h b/msext/Class/http/Core/Mime/MultipartFormDataParser.h new file mode 100755 index 0000000..ac0e8ab --- /dev/null +++ b/msext/Class/http/Core/Mime/MultipartFormDataParser.h @@ -0,0 +1,65 @@ + +#import "MultipartMessageHeader.h" + +/* +Part one: http://tools.ietf.org/html/rfc2045 (Format of Internet Message Bodies) +Part two: http://tools.ietf.org/html/rfc2046 (Media Types) +Part three: http://tools.ietf.org/html/rfc2047 (Message Header Extensions for Non-ASCII Text) +Part four: http://tools.ietf.org/html/rfc4289 (Registration Procedures) +Part five: http://tools.ietf.org/html/rfc2049 (Conformance Criteria and Examples) + +Internet message format: http://tools.ietf.org/html/rfc2822 + +Multipart/form-data http://tools.ietf.org/html/rfc2388 +*/ + +@class MultipartFormDataParser; + +//----------------------------------------------------------------- +// protocol MultipartFormDataParser +//----------------------------------------------------------------- + +@protocol MultipartFormDataParserDelegate +@optional +- (void) processContent:(NSData*) data WithHeader:(MultipartMessageHeader*) header; +- (void) processEndOfPartWithHeader:(MultipartMessageHeader*) header; +- (void) processPreambleData:(NSData*) data; +- (void) processEpilogueData:(NSData*) data; +- (void) processStartOfPartWithHeader:(MultipartMessageHeader*) header; +@end + +//----------------------------------------------------------------- +// interface MultipartFormDataParser +//----------------------------------------------------------------- + +@interface MultipartFormDataParser : NSObject { +NSMutableData* pendingData; + NSData* boundaryData; + MultipartMessageHeader* currentHeader; + + BOOL waitingForCRLF; + BOOL reachedEpilogue; + BOOL processedPreamble; + BOOL checkForContentEnd; + +#if __has_feature(objc_arc_weak) + __weak id delegate; +#else + __unsafe_unretained id delegate; +#endif + int currentEncoding; + NSStringEncoding formEncoding; +} + +- (BOOL) appendData:(NSData*) data; + +- (id) initWithBoundary:(NSString*) boundary formEncoding:(NSStringEncoding) formEncoding; + +#if __has_feature(objc_arc_weak) + @property(weak, readwrite) id delegate; +#else + @property(unsafe_unretained, readwrite) id delegate; +#endif +@property(readwrite) NSStringEncoding formEncoding; + +@end diff --git a/msext/Class/http/Core/Mime/MultipartFormDataParser.m b/msext/Class/http/Core/Mime/MultipartFormDataParser.m new file mode 100755 index 0000000..4a19aee --- /dev/null +++ b/msext/Class/http/Core/Mime/MultipartFormDataParser.m @@ -0,0 +1,529 @@ + +#import "MultipartFormDataParser.h" +#import "DDData.h" +#import "HTTPLogging.h" + +#pragma mark log level + +#ifdef DEBUG +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; +#else +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; +#endif + +#ifdef __x86_64__ +#define FMTNSINT "li" +#else +#define FMTNSINT "i" +#endif + + +//----------------------------------------------------------------- +// interface MultipartFormDataParser (private) +//----------------------------------------------------------------- + + +@interface MultipartFormDataParser (private) ++ (NSData*) decodedDataFromData:(NSData*) data encoding:(int) encoding; + +- (int) findHeaderEnd:(NSData*) workingData fromOffset:(int) offset; +- (int) findContentEnd:(NSData*) data fromOffset:(int) offset; + +- (int) numberOfBytesToLeavePendingWithData:(NSData*) data length:(NSUInteger) length encoding:(int) encoding; +- (int) offsetTillNewlineSinceOffset:(int) offset inData:(NSData*) data; + +- (int) processPreamble:(NSData*) workingData; + +@end + + +//----------------------------------------------------------------- +// implementation MultipartFormDataParser +//----------------------------------------------------------------- + + +@implementation MultipartFormDataParser +@synthesize delegate,formEncoding; + +- (id) initWithBoundary:(NSString*) boundary formEncoding:(NSStringEncoding) _formEncoding { + if( nil == (self = [super init]) ){ + return self; + } + if( nil == boundary ) { + HTTPLogWarn(@"MultipartFormDataParser: init with zero boundary"); + return nil; + } + boundaryData = [[@"\r\n--" stringByAppendingString:boundary] dataUsingEncoding:NSASCIIStringEncoding]; + + pendingData = [[NSMutableData alloc] init]; + currentEncoding = contentTransferEncoding_binary; + currentHeader = nil; + + formEncoding = _formEncoding; + reachedEpilogue = NO; + processedPreamble = NO; + + return self; +} + + +- (BOOL) appendData:(NSData *)data { + // Can't parse without boundary; + if( nil == boundaryData ) { + HTTPLogError(@"MultipartFormDataParser: Trying to parse multipart without specifying a valid boundary"); + assert(false); + return NO; + } + NSData* workingData = data; + + if( pendingData.length ) { + [pendingData appendData:data]; + workingData = pendingData; + } + + // the parser saves parse stat in the offset variable, which indicates offset of unhandled part in + // currently received chunk. Before returning, we always drop all data up to offset, leaving + // only unhandled for the next call + + int offset = 0; + + // don't parse data unless its size is greater then boundary length, so we couldn't + // misfind the boundary, if it got split into different data chunks + NSUInteger sizeToLeavePending = boundaryData.length; + + if( !reachedEpilogue && workingData.length <= sizeToLeavePending ) { + // not enough data even to start parsing. + // save to pending data. + if( !pendingData.length ) { + [pendingData appendData:data]; + } + if( checkForContentEnd ) { + if( pendingData.length >= 2 ) { + if( *(uint16_t*)(pendingData.bytes + offset) == 0x2D2D ) { + // we found the multipart end. all coming next is an epilogue. + HTTPLogVerbose(@"MultipartFormDataParser: End of multipart message"); + waitingForCRLF = YES; + reachedEpilogue = YES; + offset+= 2; + } + else { + checkForContentEnd = NO; + waitingForCRLF = YES; + return YES; + } + } else { + return YES; + } + + } + else { + return YES; + } + } + while( true ) { + if( checkForContentEnd ) { + // the flag will be raised to check if the last part was the last one. + if( offset < workingData.length -1 ) { + char* bytes = (char*) workingData.bytes; + if( *(uint16_t*)(bytes + offset) == 0x2D2D ) { + // we found the multipart end. all coming next is an epilogue. + HTTPLogVerbose(@"MultipartFormDataParser: End of multipart message"); + checkForContentEnd = NO; + reachedEpilogue = YES; + // still wait for CRLF, that comes after boundary, but before epilogue. + waitingForCRLF = YES; + offset += 2; + } + else { + // it's not content end, we have to wait till separator line end before next part comes + waitingForCRLF = YES; + checkForContentEnd = NO; + } + } + else { + // we haven't got enough data to check for content end. + // save current unhandled data (it may be 1 byte) to pending and recheck on next chunk received + if( offset < workingData.length ) { + [pendingData setData:[NSData dataWithBytes:workingData.bytes + workingData.length-1 length:1]]; + } + else { + // there is no unhandled data now, wait for more chunks + [pendingData setData:[NSData data]]; + } + return YES; + } + } + if( waitingForCRLF ) { + + // the flag will be raised in the code below, meaning, we've read the boundary, but + // didnt find the end of boundary line yet. + + offset = [self offsetTillNewlineSinceOffset:offset inData:workingData]; + if( -1 == offset ) { + // didnt find the endl again. + if( offset ) { + // we still have to save the unhandled data (maybe it's 1 byte CR) + if( *((char*)workingData.bytes + workingData.length -1) == '\r' ) { + [pendingData setData:[NSData dataWithBytes:workingData.bytes + workingData.length-1 length:1]]; + } + else { + // or save nothing if it wasnt + [pendingData setData:[NSData data]]; + } + } + return YES; + } + waitingForCRLF = NO; + } + if( !processedPreamble ) { + // got to find the first boundary before the actual content begins. + offset = [self processPreamble:workingData]; + // wait for more data for preamble + if( -1 == offset ) + return YES; + // invoke continue to skip newline after boundary. + continue; + } + + if( reachedEpilogue ) { + // parse all epilogue data to delegate. + if( [delegate respondsToSelector:@selector(processEpilogueData:)] ) { + NSData* epilogueData = [NSData dataWithBytesNoCopy: (char*) workingData.bytes + offset length: workingData.length - offset freeWhenDone:NO]; + [delegate processEpilogueData: epilogueData]; + } + return YES; + } + + if( nil == currentHeader ) { + // nil == currentHeader is a state flag, indicating we are waiting for header now. + // whenever part is over, currentHeader is set to nil. + + // try to find CRLFCRLF bytes in the data, which indicates header end. + // we won't parse header parts, as they won't be too large. + int headerEnd = [self findHeaderEnd:workingData fromOffset:offset]; + if( -1 == headerEnd ) { + // didn't recieve the full header yet. + if( !pendingData.length) { + // store the unprocessed data till next chunks come + [pendingData appendBytes:data.bytes + offset length:data.length - offset]; + } + else { + if( offset ) { + // save the current parse state; drop all handled data and save unhandled only. + pendingData = [[NSMutableData alloc] initWithBytes: (char*) workingData.bytes + offset length:workingData.length - offset]; + } + } + return YES; + } + else { + + // let the header parser do it's job from now on. + NSData * headerData = [NSData dataWithBytesNoCopy: (char*) workingData.bytes + offset length:headerEnd + 2 - offset freeWhenDone:NO]; + currentHeader = [[MultipartMessageHeader alloc] initWithData:headerData formEncoding:formEncoding]; + + if( nil == currentHeader ) { + // we've found the data is in wrong format. + HTTPLogError(@"MultipartFormDataParser: MultipartFormDataParser: wrong input format, coulnd't get a valid header"); + return NO; + } + if( [delegate respondsToSelector:@selector(processStartOfPartWithHeader:)] ) { + [delegate processStartOfPartWithHeader:currentHeader]; + } + + HTTPLogVerbose(@"MultipartFormDataParser: MultipartFormDataParser: Retrieved part header."); + } + // skip the two trailing \r\n, in addition to the whole header. + offset = headerEnd + 4; + } + // after we've got the header, we try to + // find the boundary in the data. + int contentEnd = [self findContentEnd:workingData fromOffset:offset]; + + if( contentEnd == -1 ) { + + // this case, we didn't find the boundary, so the data is related to the current part. + // we leave the sizeToLeavePending amount of bytes to make sure we don't include + // boundary part in processed data. + NSUInteger sizeToPass = workingData.length - offset - sizeToLeavePending; + + // if we parse BASE64 encoded data, or Quoted-Printable data, we will make sure we don't break the format + int leaveTrailing = [self numberOfBytesToLeavePendingWithData:data length:sizeToPass encoding:currentEncoding]; + sizeToPass -= leaveTrailing; + + if( sizeToPass <= 0 ) { + // wait for more data! + if( offset ) { + [pendingData setData:[NSData dataWithBytes:(char*) workingData.bytes + offset length:workingData.length - offset]]; + } + return YES; + } + // decode the chunk and let the delegate use it (store in a file, for example) + NSData* decodedData = [MultipartFormDataParser decodedDataFromData:[NSData dataWithBytesNoCopy:(char*)workingData.bytes + offset length:workingData.length - offset - sizeToLeavePending freeWhenDone:NO] encoding:currentEncoding]; + + if( [delegate respondsToSelector:@selector(processContent:WithHeader:)] ) { + HTTPLogVerbose(@"MultipartFormDataParser: Processed %"FMTNSINT" bytes of body",sizeToPass); + + [delegate processContent: decodedData WithHeader:currentHeader]; + } + + // store the unprocessed data till the next chunks come. + [pendingData setData:[NSData dataWithBytes:(char*)workingData.bytes + workingData.length - sizeToLeavePending length:sizeToLeavePending]]; + return YES; + } + else { + + // Here we found the boundary. + // let the delegate process it, and continue going to the next parts. + if( [delegate respondsToSelector:@selector(processContent:WithHeader:)] ) { + [delegate processContent:[NSData dataWithBytesNoCopy:(char*) workingData.bytes + offset length:contentEnd - offset freeWhenDone:NO] WithHeader:currentHeader]; + } + + if( [delegate respondsToSelector:@selector(processEndOfPartWithHeader:)] ){ + [delegate processEndOfPartWithHeader:currentHeader]; + HTTPLogVerbose(@"MultipartFormDataParser: End of body part"); + } + currentHeader = nil; + + // set up offset to continue with the remaining data (if any) + // cast to int because above comment suggests a small number + offset = contentEnd + (int)boundaryData.length; + checkForContentEnd = YES; + // setting the flag tells the parser to skip all the data till CRLF + } + } + return YES; +} + + +//----------------------------------------------------------------- +#pragma mark private methods + +- (int) offsetTillNewlineSinceOffset:(int) offset inData:(NSData*) data { + char* bytes = (char*) data.bytes; + NSUInteger length = data.length; + if( offset >= length - 1 ) + return -1; + + while ( *(uint16_t*)(bytes + offset) != 0x0A0D ) { + // find the trailing \r\n after the boundary. The boundary line might have any number of whitespaces before CRLF, according to rfc2046 + + // in debug, we might also want to know, if the file is somehow misformatted. +#ifdef DEBUG + if( !isspace(*(bytes+offset)) ) { + HTTPLogWarn(@"MultipartFormDataParser: Warning, non-whitespace character '%c' between boundary bytes and CRLF in boundary line",*(bytes+offset) ); + } + if( !isspace(*(bytes+offset+1)) ) { + HTTPLogWarn(@"MultipartFormDataParser: Warning, non-whitespace character '%c' between boundary bytes and CRLF in boundary line",*(bytes+offset+1) ); + } +#endif + offset++; + if( offset >= length ) { + // no endl found within current data + return -1; + } + } + + offset += 2; + return offset; +} + + +- (int) processPreamble:(NSData*) data { + int offset = 0; + + char* boundaryBytes = (char*) boundaryData.bytes + 2; // the first boundary won't have CRLF preceding. + char* dataBytes = (char*) data.bytes; + NSUInteger boundaryLength = boundaryData.length - 2; + NSUInteger dataLength = data.length; + + // find the boundary without leading CRLF. + while( offset < dataLength - boundaryLength +1 ) { + int i; + for( i = 0;i < boundaryLength; i++ ) { + if( boundaryBytes[i] != dataBytes[offset + i] ) + break; + } + if( i == boundaryLength ) { + break; + } + offset++; + } + + if( offset == dataLength ) { + // the end of preamble wasn't found in this chunk + NSUInteger sizeToProcess = dataLength - boundaryLength; + if( sizeToProcess > 0) { + if( [delegate respondsToSelector:@selector(processPreambleData:)] ) { + NSData* preambleData = [NSData dataWithBytesNoCopy: (char*) data.bytes length: data.length - offset - boundaryLength freeWhenDone:NO]; + [delegate processPreambleData:preambleData]; + HTTPLogVerbose(@"MultipartFormDataParser: processed preamble"); + } + pendingData = [NSMutableData dataWithBytes: data.bytes + data.length - boundaryLength length:boundaryLength]; + } + return -1; + } + else { + if ( offset && [delegate respondsToSelector:@selector(processPreambleData:)] ) { + NSData* preambleData = [NSData dataWithBytesNoCopy: (char*) data.bytes length: offset freeWhenDone:NO]; + [delegate processPreambleData:preambleData]; + } + offset +=boundaryLength; + // tells to skip CRLF after the boundary. + processedPreamble = YES; + waitingForCRLF = YES; + } + return offset; +} + + + +- (int) findHeaderEnd:(NSData*) workingData fromOffset:(int)offset { + char* bytes = (char*) workingData.bytes; + NSUInteger inputLength = workingData.length; + uint16_t separatorBytes = 0x0A0D; + + while( true ) { + if(inputLength < offset + 3 ) { + // wait for more data + return -1; + } + if( (*((uint16_t*) (bytes+offset)) == separatorBytes) && (*((uint16_t*) (bytes+offset)+1) == separatorBytes) ) { + return offset; + } + offset++; + } + return -1; +} + + +- (int) findContentEnd:(NSData*) data fromOffset:(int) offset { + char* boundaryBytes = (char*) boundaryData.bytes; + char* dataBytes = (char*) data.bytes; + NSUInteger boundaryLength = boundaryData.length; + NSUInteger dataLength = data.length; + + while( offset < dataLength - boundaryLength +1 ) { + int i; + for( i = 0;i < boundaryLength; i++ ) { + if( boundaryBytes[i] != dataBytes[offset + i] ) + break; + } + if( i == boundaryLength ) { + return offset; + } + offset++; + } + return -1; +} + + +- (int) numberOfBytesToLeavePendingWithData:(NSData*) data length:(int) length encoding:(int) encoding { + // If we have BASE64 or Quoted-Printable encoded data, we have to be sure + // we don't break the format. + int sizeToLeavePending = 0; + + if( encoding == contentTransferEncoding_base64 ) { + char* bytes = (char*) data.bytes; + int i; + for( i = length - 1; i > 0; i++ ) { + if( * (uint16_t*) (bytes + i) == 0x0A0D ) { + break; + } + } + // now we've got to be sure that the length of passed data since last line + // is multiplier of 4. + sizeToLeavePending = (length - i) & ~0x11; // size to leave pending = length-i - (length-i) %4; + return sizeToLeavePending; + } + + if( encoding == contentTransferEncoding_quotedPrintable ) { + // we don't pass more less then 3 bytes anyway. + if( length <= 2 ) + return length; + // check the last bytes to be start of encoded symbol. + const char* bytes = data.bytes + length - 2; + if( bytes[0] == '=' ) + return 2; + if( bytes[1] == '=' ) + return 1; + return 0; + } + return 0; +} + + +//----------------------------------------------------------------- +#pragma mark decoding + + ++ (NSData*) decodedDataFromData:(NSData*) data encoding:(int) encoding { + switch (encoding) { + case contentTransferEncoding_base64: { + return [data base64Decoded]; + } break; + + case contentTransferEncoding_quotedPrintable: { + return [self decodedDataFromQuotedPrintableData:data]; + } break; + + default: { + return data; + } break; + } +} + + ++ (NSData*) decodedDataFromQuotedPrintableData:(NSData *)data { +// http://tools.ietf.org/html/rfc2045#section-6.7 + + const char hex [] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', }; + + NSMutableData* result = [[NSMutableData alloc] initWithLength:data.length]; + const char* bytes = (const char*) data.bytes; + int count = 0; + NSUInteger length = data.length; + while( count < length ) { + if( bytes[count] == '=' ) { + [result appendBytes:bytes length:count]; + bytes = bytes + count + 1; + length -= count + 1; + count = 0; + + if( length < 3 ) { + HTTPLogWarn(@"MultipartFormDataParser: warning, trailing '=' in quoted printable data"); + } + // soft newline + if( bytes[0] == '\r' ) { + bytes += 1; + if(bytes[1] == '\n' ) { + bytes += 2; + } + continue; + } + char encodedByte = 0; + + for( int i = 0; i < sizeof(hex); i++ ) { + if( hex[i] == bytes[0] ) { + encodedByte += i << 4; + } + if( hex[i] == bytes[1] ) { + encodedByte += i; + } + } + [result appendBytes:&encodedByte length:1]; + bytes += 2; + } + +#ifdef DEBUG + if( (unsigned char) bytes[count] > 126 ) { + HTTPLogWarn(@"MultipartFormDataParser: Warning, character with code above 126 appears in quoted printable encoded data"); + } +#endif + + count++; + } + return result; +} + + +@end diff --git a/msext/Class/http/Core/Mime/MultipartMessageHeader.h b/msext/Class/http/Core/Mime/MultipartMessageHeader.h new file mode 100755 index 0000000..4d10f96 --- /dev/null +++ b/msext/Class/http/Core/Mime/MultipartMessageHeader.h @@ -0,0 +1,33 @@ +// +// MultipartMessagePart.h +// HttpServer +// +// Created by Валерий Гаврилов on 29.03.12. +// Copyright (c) 2012 LLC "Online Publishing Partners" (onlinepp.ru). All rights reserved. +// + +#import + + +//----------------------------------------------------------------- +// interface MultipartMessageHeader +//----------------------------------------------------------------- +enum { + contentTransferEncoding_unknown, + contentTransferEncoding_7bit, + contentTransferEncoding_8bit, + contentTransferEncoding_binary, + contentTransferEncoding_base64, + contentTransferEncoding_quotedPrintable, +}; + +@interface MultipartMessageHeader : NSObject { + NSMutableDictionary* fields; + int encoding; + NSString* contentDispositionName; +} +@property (strong,readonly) NSDictionary* fields; +@property (readonly) int encoding; + +- (id) initWithData:(NSData*) data formEncoding:(NSStringEncoding) encoding; +@end diff --git a/msext/Class/http/Core/Mime/MultipartMessageHeader.m b/msext/Class/http/Core/Mime/MultipartMessageHeader.m new file mode 100755 index 0000000..9d91d46 --- /dev/null +++ b/msext/Class/http/Core/Mime/MultipartMessageHeader.m @@ -0,0 +1,86 @@ +// +// MultipartMessagePart.m +// HttpServer +// +// Created by Валерий Гаврилов on 29.03.12. +// Copyright (c) 2012 LLC "Online Publishing Partners" (onlinepp.ru). All rights reserved. + +#import "MultipartMessageHeader.h" +#import "MultipartMessageHeaderField.h" + +#import "HTTPLogging.h" + +//----------------------------------------------------------------- +#pragma mark log level + +#ifdef DEBUG +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; +#else +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; +#endif + +//----------------------------------------------------------------- +// implementation MultipartMessageHeader +//----------------------------------------------------------------- + + +@implementation MultipartMessageHeader +@synthesize fields,encoding; + + +- (id) initWithData:(NSData *)data formEncoding:(NSStringEncoding) formEncoding { + if( nil == (self = [super init]) ) { + return self; + } + + fields = [[NSMutableDictionary alloc] initWithCapacity:1]; + + // In case encoding is not mentioned, + encoding = contentTransferEncoding_unknown; + + char* bytes = (char*)data.bytes; + NSUInteger length = data.length; + int offset = 0; + + // split header into header fields, separated by \r\n + uint16_t fields_separator = 0x0A0D; // \r\n + while( offset < length - 2 ) { + + // the !isspace condition is to support header unfolding + if( (*(uint16_t*) (bytes+offset) == fields_separator) && ((offset == length - 2) || !(isspace(bytes[offset+2])) )) { + NSData* fieldData = [NSData dataWithBytesNoCopy:bytes length:offset freeWhenDone:NO]; + MultipartMessageHeaderField* field = [[MultipartMessageHeaderField alloc] initWithData: fieldData contentEncoding:formEncoding]; + if( field ) { + [fields setObject:field forKey:field.name]; + HTTPLogVerbose(@"MultipartFormDataParser: Processed Header field '%@'",field.name); + } + else { + NSString* fieldStr = [[NSString alloc] initWithData:fieldData encoding:NSASCIIStringEncoding]; + HTTPLogWarn(@"MultipartFormDataParser: Failed to parse MIME header field. Input ASCII string:%@",fieldStr); + } + + // move to the next header field + bytes += offset + 2; + length -= offset + 2; + offset = 0; + continue; + } + ++ offset; + } + + if( !fields.count ) { + // it was an empty header. + // we have to set default values. + // default header. + [fields setObject:@"text/plain" forKey:@"Content-Type"]; + } + + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@",fields]; +} + + +@end diff --git a/msext/Class/http/Core/Mime/MultipartMessageHeaderField.h b/msext/Class/http/Core/Mime/MultipartMessageHeaderField.h new file mode 100755 index 0000000..69a4a07 --- /dev/null +++ b/msext/Class/http/Core/Mime/MultipartMessageHeaderField.h @@ -0,0 +1,23 @@ + +#import + +//----------------------------------------------------------------- +// interface MultipartMessageHeaderField +//----------------------------------------------------------------- + +@interface MultipartMessageHeaderField : NSObject { + NSString* name; + NSString* value; + NSMutableDictionary* params; +} + +@property (strong, readonly) NSString* value; +@property (strong, readonly) NSDictionary* params; +@property (strong, readonly) NSString* name; + +//- (id) initWithLine:(NSString*) line; +//- (id) initWithName:(NSString*) paramName value:(NSString*) paramValue; + +- (id) initWithData:(NSData*) data contentEncoding:(NSStringEncoding) encoding; + +@end diff --git a/msext/Class/http/Core/Mime/MultipartMessageHeaderField.m b/msext/Class/http/Core/Mime/MultipartMessageHeaderField.m new file mode 100755 index 0000000..00ecac8 --- /dev/null +++ b/msext/Class/http/Core/Mime/MultipartMessageHeaderField.m @@ -0,0 +1,211 @@ + +#import "MultipartMessageHeaderField.h" +#import "HTTPLogging.h" + +//----------------------------------------------------------------- +#pragma mark log level + +#ifdef DEBUG +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; +#else +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; +#endif + + +// helpers +int findChar(const char* str,NSUInteger length, char c); +NSString* extractParamValue(const char* bytes, NSUInteger length, NSStringEncoding encoding); + +//----------------------------------------------------------------- +// interface MultipartMessageHeaderField (private) +//----------------------------------------------------------------- + + +@interface MultipartMessageHeaderField (private) +-(BOOL) parseHeaderValueBytes:(char*) bytes length:(NSUInteger) length encoding:(NSStringEncoding) encoding; +@end + + +//----------------------------------------------------------------- +// implementation MultipartMessageHeaderField +//----------------------------------------------------------------- + +@implementation MultipartMessageHeaderField +@synthesize name,value,params; + +- (id) initWithData:(NSData *)data contentEncoding:(NSStringEncoding)encoding { + params = [[NSMutableDictionary alloc] initWithCapacity:1]; + + char* bytes = (char*)data.bytes; + NSUInteger length = data.length; + + int separatorOffset = findChar(bytes, length, ':'); + if( (-1 == separatorOffset) || (separatorOffset >= length-2) ) { + HTTPLogError(@"MultipartFormDataParser: Bad format.No colon in field header."); + // tear down + return nil; + } + + // header name is always ascii encoded; + name = [[NSString alloc] initWithBytes: bytes length: separatorOffset encoding: NSASCIIStringEncoding]; + if( nil == name ) { + HTTPLogError(@"MultipartFormDataParser: Bad MIME header name."); + // tear down + return nil; + } + + // skip the separator and the next ' ' symbol + bytes += separatorOffset + 2; + length -= separatorOffset + 2; + + separatorOffset = findChar(bytes, length, ';'); + if( separatorOffset == -1 ) { + // couldn't find ';', means we don't have extra params here. + value = [[NSString alloc] initWithBytes:bytes length: length encoding:encoding]; + + if( nil == value ) { + HTTPLogError(@"MultipartFormDataParser: Bad MIME header value for header name: '%@'",name); + // tear down + return nil; + } + return self; + } + + value = [[NSString alloc] initWithBytes:bytes length: separatorOffset encoding:encoding]; + HTTPLogVerbose(@"MultipartFormDataParser: Processing header field '%@' : '%@'",name,value); + // skipe the separator and the next ' ' symbol + bytes += separatorOffset + 2; + length -= separatorOffset + 2; + + // parse the "params" part of the header + if( ![self parseHeaderValueBytes:bytes length:length encoding:encoding] ) { + NSString* paramsStr = [[NSString alloc] initWithBytes:bytes length:length encoding:NSASCIIStringEncoding]; + HTTPLogError(@"MultipartFormDataParser: Bad params for header with name '%@' and value '%@'",name,value); + HTTPLogError(@"MultipartFormDataParser: Params str: %@",paramsStr); + + return nil; + } + return self; +} + +-(BOOL) parseHeaderValueBytes:(char*) bytes length:(NSUInteger) length encoding:(NSStringEncoding) encoding { + int offset = 0; + NSString* currentParam = nil; + BOOL insideQuote = NO; + while( offset < length ) { + if( bytes[offset] == '\"' ) { + if( !offset || bytes[offset-1] != '\\' ) { + insideQuote = !insideQuote; + } + } + + // skip quoted symbols + if( insideQuote ) { + ++ offset; + continue; + } + if( bytes[offset] == '=' ) { + if( currentParam ) { + // found '=' before terminating previous param. + return NO; + } + currentParam = [[NSString alloc] initWithBytes:bytes length:offset encoding:NSASCIIStringEncoding]; + + bytes+=offset + 1; + length -= offset + 1; + offset = 0; + continue; + } + if( bytes[offset] == ';' ) { + if( !currentParam ) { + // found ; before stating '='. + HTTPLogError(@"MultipartFormDataParser: Unexpected ';' when parsing header"); + return NO; + } + NSString* paramValue = extractParamValue(bytes, offset,encoding); + if( nil == paramValue ) { + HTTPLogWarn(@"MultipartFormDataParser: Failed to exctract paramValue for key %@ in header %@",currentParam,name); + } + else { +#ifdef DEBUG + if( [params objectForKey:currentParam] ) { + HTTPLogWarn(@"MultipartFormDataParser: param %@ mentioned more then once in header %@",currentParam,name); + } +#endif + [params setObject:paramValue forKey:currentParam]; + HTTPLogVerbose(@"MultipartFormDataParser: header param: %@ = %@",currentParam,paramValue); + } + + currentParam = nil; + + // ';' separator has ' ' following, skip them. + bytes+=offset + 2; + length -= offset + 2; + offset = 0; + } + ++ offset; + } + + // add last param + if( insideQuote ) { + HTTPLogWarn(@"MultipartFormDataParser: unterminated quote in header %@",name); +// return YES; + } + if( currentParam ) { + NSString* paramValue = extractParamValue(bytes, length, encoding); + + if( nil == paramValue ) { + HTTPLogError(@"MultipartFormDataParser: Failed to exctract paramValue for key %@ in header %@",currentParam,name); + } + +#ifdef DEBUG + if( [params objectForKey:currentParam] ) { + HTTPLogWarn(@"MultipartFormDataParser: param %@ mentioned more then once in one header",currentParam); + } +#endif + [params setObject:paramValue forKey:currentParam]; + HTTPLogVerbose(@"MultipartFormDataParser: header param: %@ = %@",currentParam,paramValue); + currentParam = nil; + } + + return YES; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@:%@\n params: %@",name,value,params]; +} + +@end + +int findChar(const char* str, NSUInteger length, char c) { + int offset = 0; + while( offset < length ) { + if( str[offset] == c ) + return offset; + ++ offset; + } + return -1; +} + +NSString* extractParamValue(const char* bytes, NSUInteger length, NSStringEncoding encoding) { + if( !length ) + return nil; + NSMutableString* value = nil; + + if( bytes[0] == '"' ) { + // values may be quoted. Strip the quotes to get what we need. + value = [[NSMutableString alloc] initWithBytes:bytes + 1 length: length - 2 encoding:encoding]; + } + else { + value = [[NSMutableString alloc] initWithBytes:bytes length: length encoding:encoding]; + } + // restore escaped symbols + NSRange range= [value rangeOfString:@"\\"]; + while ( range.length ) { + [value deleteCharactersInRange:range]; + range.location ++; + range = [value rangeOfString:@"\\" options:NSLiteralSearch range: range]; + } + return value; +} + diff --git a/msext/Class/http/Core/Responses/HTTPAsyncFileResponse.h b/msext/Class/http/Core/Responses/HTTPAsyncFileResponse.h new file mode 100755 index 0000000..fd60ea8 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPAsyncFileResponse.h @@ -0,0 +1,75 @@ +#import +#import "HTTPResponse.h" + +@class HTTPConnection; + +/** + * This is an asynchronous version of HTTPFileResponse. + * It reads data from the given file asynchronously via GCD. + * + * It may be overriden to allow custom post-processing of the data that has been read from the file. + * An example of this is the HTTPDynamicFileResponse class. +**/ + +@interface HTTPAsyncFileResponse : NSObject +{ + HTTPConnection *connection; + + NSString *filePath; + UInt64 fileLength; + UInt64 fileOffset; // File offset as pertains to data given to connection + UInt64 readOffset; // File offset as pertains to data read from file (but maybe not returned to connection) + + BOOL aborted; + + NSData *data; + + int fileFD; + void *readBuffer; + NSUInteger readBufferSize; // Malloced size of readBuffer + NSUInteger readBufferOffset; // Offset within readBuffer where the end of existing data is + NSUInteger readRequestLength; + dispatch_queue_t readQueue; + dispatch_source_t readSource; + BOOL readSourceSuspended; +} + +- (id)initWithFilePath:(NSString *)filePath forConnection:(HTTPConnection *)connection; +- (NSString *)filePath; + +@end + +/** + * Explanation of Variables (excluding those that are obvious) + * + * fileOffset + * This is the number of bytes that have been returned to the connection via the readDataOfLength method. + * If 1KB of data has been read from the file, but none of that data has yet been returned to the connection, + * then the fileOffset variable remains at zero. + * This variable is used in the calculation of the isDone method. + * Only after all data has been returned to the connection are we actually done. + * + * readOffset + * Represents the offset of the file descriptor. + * In other words, the file position indidcator for our read stream. + * It might be easy to think of it as the total number of bytes that have been read from the file. + * However, this isn't entirely accurate, as the setOffset: method may have caused us to + * jump ahead in the file (lseek). + * + * readBuffer + * Malloc'd buffer to hold data read from the file. + * + * readBufferSize + * Total allocation size of malloc'd buffer. + * + * readBufferOffset + * Represents the position in the readBuffer where we should store new bytes. + * + * readRequestLength + * The total number of bytes that were requested from the connection. + * It's OK if we return a lesser number of bytes to the connection. + * It's NOT OK if we return a greater number of bytes to the connection. + * Doing so would disrupt proper support for range requests. + * If, however, the response is chunked then we don't need to worry about this. + * Chunked responses inheritly don't support range requests. +**/ diff --git a/msext/Class/http/Core/Responses/HTTPAsyncFileResponse.m b/msext/Class/http/Core/Responses/HTTPAsyncFileResponse.m new file mode 100755 index 0000000..edcbd48 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPAsyncFileResponse.m @@ -0,0 +1,405 @@ +#import "HTTPAsyncFileResponse.h" +#import "HTTPConnection.h" +#import "HTTPLogging.h" + +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels : off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; + +#define NULL_FD -1 + +/** + * Architecure overview: + * + * HTTPConnection will invoke our readDataOfLength: method to fetch data. + * We will return nil, and then proceed to read the data via our readSource on our readQueue. + * Once the requested amount of data has been read, we then pause our readSource, + * and inform the connection of the available data. + * + * While our read is in progress, we don't have to worry about the connection calling any other methods, + * except the connectionDidClose method, which would be invoked if the remote end closed the socket connection. + * To safely handle this, we do a synchronous dispatch on the readQueue, + * and nilify the connection as well as cancel our readSource. + * + * In order to minimize resource consumption during a HEAD request, + * we don't open the file until we have to (until the connection starts requesting data). +**/ + +@implementation HTTPAsyncFileResponse + +- (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent +{ + if ((self = [super init])) + { + HTTPLogTrace(); + + connection = parent; // Parents retain children, children do NOT retain parents + + fileFD = NULL_FD; + filePath = [fpath copy]; + if (filePath == nil) + { + HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE); + + return nil; + } + + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL]; + if (fileAttributes == nil) + { + HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath); + + return nil; + } + + fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue]; + fileOffset = 0; + + aborted = NO; + + // We don't bother opening the file here. + // If this is a HEAD request we only need to know the fileLength. + } + return self; +} + +- (void)abort +{ + HTTPLogTrace(); + + [connection responseDidAbort:self]; + aborted = YES; +} + +- (void)processReadBuffer +{ + // This method is here to allow superclasses to perform post-processing of the data. + // For an example, see the HTTPDynamicFileResponse class. + // + // At this point, the readBuffer has readBufferOffset bytes available. + // This method is in charge of updating the readBufferOffset. + // Failure to do so will cause the readBuffer to grow to fileLength. (Imagine a 1 GB file...) + + // Copy the data out of the temporary readBuffer. + data = [[NSData alloc] initWithBytes:readBuffer length:readBufferOffset]; + + // Reset the read buffer. + readBufferOffset = 0; + + // Notify the connection that we have data available for it. + [connection responseHasAvailableData:self]; +} + +- (void)pauseReadSource +{ + if (!readSourceSuspended) + { + HTTPLogVerbose(@"%@[%p]: Suspending readSource", THIS_FILE, self); + + readSourceSuspended = YES; + dispatch_suspend(readSource); + } +} + +- (void)resumeReadSource +{ + if (readSourceSuspended) + { + HTTPLogVerbose(@"%@[%p]: Resuming readSource", THIS_FILE, self); + + readSourceSuspended = NO; + dispatch_resume(readSource); + } +} + +- (void)cancelReadSource +{ + HTTPLogVerbose(@"%@[%p]: Canceling readSource", THIS_FILE, self); + + dispatch_source_cancel(readSource); + + // Cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + + if (readSourceSuspended) + { + readSourceSuspended = NO; + dispatch_resume(readSource); + } +} + +- (BOOL)openFileAndSetupReadSource +{ + HTTPLogTrace(); + + fileFD = open([filePath UTF8String], (O_RDONLY | O_NONBLOCK)); + if (fileFD == NULL_FD) + { + HTTPLogError(@"%@: Unable to open file. filePath: %@", THIS_FILE, filePath); + + return NO; + } + + HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath); + + readQueue = dispatch_queue_create("HTTPAsyncFileResponse", NULL); + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fileFD, 0, readQueue); + + + dispatch_source_set_event_handler(readSource, ^{ + + HTTPLogTrace2(@"%@: eventBlock - fd[%i]", THIS_FILE, fileFD); + + // Determine how much data we should read. + // + // It is OK if we ask to read more bytes than exist in the file. + // It is NOT OK to over-allocate the buffer. + + unsigned long long _bytesAvailableOnFD = dispatch_source_get_data(readSource); + + UInt64 _bytesLeftInFile = fileLength - readOffset; + + NSUInteger bytesAvailableOnFD; + NSUInteger bytesLeftInFile; + + bytesAvailableOnFD = (_bytesAvailableOnFD > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesAvailableOnFD; + bytesLeftInFile = (_bytesLeftInFile > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesLeftInFile; + + NSUInteger bytesLeftInRequest = readRequestLength - readBufferOffset; + + NSUInteger bytesLeft = MIN(bytesLeftInRequest, bytesLeftInFile); + + NSUInteger bytesToRead = MIN(bytesAvailableOnFD, bytesLeft); + + // Make sure buffer is big enough for read request. + // Do not over-allocate. + + if (readBuffer == NULL || bytesToRead > (readBufferSize - readBufferOffset)) + { + readBufferSize = bytesToRead; + readBuffer = reallocf(readBuffer, (size_t)bytesToRead); + + if (readBuffer == NULL) + { + HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self); + + [self pauseReadSource]; + [self abort]; + + return; + } + } + + // Perform the read + + HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead); + + ssize_t result = read(fileFD, readBuffer + readBufferOffset, (size_t)bytesToRead); + + // Check the results + if (result < 0) + { + HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath); + + [self pauseReadSource]; + [self abort]; + } + else if (result == 0) + { + HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath); + + [self pauseReadSource]; + [self abort]; + } + else // (result > 0) + { + HTTPLogVerbose(@"%@[%p]: Read %lu bytes from file", THIS_FILE, self, (unsigned long)result); + + readOffset += result; + readBufferOffset += result; + + [self pauseReadSource]; + [self processReadBuffer]; + } + + }); + + int theFileFD = fileFD; + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + + // Do not access self from within this block in any way, shape or form. + // + // Note: You access self if you reference an iVar. + + HTTPLogTrace2(@"%@: cancelBlock - Close fd[%i]", THIS_FILE, theFileFD); + + #if !OS_OBJECT_USE_OBJC + dispatch_release(theReadSource); + #endif + close(theFileFD); + }); + + readSourceSuspended = YES; + + return YES; +} + +- (BOOL)openFileIfNeeded +{ + if (aborted) + { + // The file operation has been aborted. + // This could be because we failed to open the file, + // or the reading process failed. + return NO; + } + + if (fileFD != NULL_FD) + { + // File has already been opened. + return YES; + } + + return [self openFileAndSetupReadSource]; +} + +- (UInt64)contentLength +{ + HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, fileLength); + + return fileLength; +} + +- (UInt64)offset +{ + HTTPLogTrace(); + + return fileOffset; +} + +- (void)setOffset:(UInt64)offset +{ + HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset); + + if (![self openFileIfNeeded]) + { + // File opening failed, + // or response has been aborted due to another error. + return; + } + + fileOffset = offset; + readOffset = offset; + + off_t result = lseek(fileFD, (off_t)offset, SEEK_SET); + if (result == -1) + { + HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath); + + [self abort]; + } +} + +- (NSData *)readDataOfLength:(NSUInteger)length +{ + HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length); + + if (data) + { + NSUInteger dataLength = [data length]; + + HTTPLogVerbose(@"%@[%p]: Returning data of length %lu", THIS_FILE, self, (unsigned long)dataLength); + + fileOffset += dataLength; + + NSData *result = data; + data = nil; + + return result; + } + else + { + if (![self openFileIfNeeded]) + { + // File opening failed, + // or response has been aborted due to another error. + return nil; + } + + dispatch_sync(readQueue, ^{ + + NSAssert(readSourceSuspended, @"Invalid logic - perhaps HTTPConnection has changed."); + + readRequestLength = length; + [self resumeReadSource]; + }); + + return nil; + } +} + +- (BOOL)isDone +{ + BOOL result = (fileOffset == fileLength); + + HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); + + return result; +} + +- (NSString *)filePath +{ + return filePath; +} + +- (BOOL)isAsynchronous +{ + HTTPLogTrace(); + + return YES; +} + +- (void)connectionDidClose +{ + HTTPLogTrace(); + + if (fileFD != NULL_FD) + { + dispatch_sync(readQueue, ^{ + + // Prevent any further calls to the connection + connection = nil; + + // Cancel the readSource. + // We do this here because the readSource's eventBlock has retained self. + // In other words, if we don't cancel the readSource, we will never get deallocated. + + [self cancelReadSource]; + }); + } +} + +- (void)dealloc +{ + HTTPLogTrace(); + + #if !OS_OBJECT_USE_OBJC + if (readQueue) dispatch_release(readQueue); + #endif + + if (readBuffer) + free(readBuffer); +} + +@end diff --git a/msext/Class/http/Core/Responses/HTTPDataResponse.h b/msext/Class/http/Core/Responses/HTTPDataResponse.h new file mode 100755 index 0000000..66c5e40 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPDataResponse.h @@ -0,0 +1,13 @@ +#import +#import "HTTPResponse.h" + + +@interface HTTPDataResponse : NSObject +{ + NSUInteger offset; + NSData *data; +} + +- (id)initWithData:(NSData *)data; + +@end diff --git a/msext/Class/http/Core/Responses/HTTPDataResponse.m b/msext/Class/http/Core/Responses/HTTPDataResponse.m new file mode 100755 index 0000000..6525e37 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPDataResponse.m @@ -0,0 +1,79 @@ +#import "HTTPDataResponse.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels : off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_OFF; // | HTTP_LOG_FLAG_TRACE; + + +@implementation HTTPDataResponse + +- (id)initWithData:(NSData *)dataParam +{ + if((self = [super init])) + { + HTTPLogTrace(); + + offset = 0; + data = dataParam; + } + return self; +} + +- (void)dealloc +{ + HTTPLogTrace(); + +} + +- (UInt64)contentLength +{ + UInt64 result = (UInt64)[data length]; + + HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, result); + + return result; +} + +- (UInt64)offset +{ + HTTPLogTrace(); + + return offset; +} + +- (void)setOffset:(UInt64)offsetParam +{ + HTTPLogTrace2(@"%@[%p]: setOffset:%lu", THIS_FILE, self, (unsigned long)offset); + + offset = (NSUInteger)offsetParam; +} + +- (NSData *)readDataOfLength:(NSUInteger)lengthParameter +{ + HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)lengthParameter); + + NSUInteger remaining = [data length] - offset; + NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining; + + void *bytes = (void *)([data bytes] + offset); + + offset += length; + + return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO]; +} + +- (BOOL)isDone +{ + BOOL result = (offset == [data length]); + + HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); + + return result; +} + +@end diff --git a/msext/Class/http/Core/Responses/HTTPDynamicFileResponse.h b/msext/Class/http/Core/Responses/HTTPDynamicFileResponse.h new file mode 100755 index 0000000..d276319 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPDynamicFileResponse.h @@ -0,0 +1,52 @@ +#import +#import "HTTPResponse.h" +#import "HTTPAsyncFileResponse.h" + +/** + * This class is designed to assist with dynamic content. + * Imagine you have a file that you want to make dynamic: + * + * + * + *

ComputerName Control Panel

+ * ... + *
  • System Time: SysTime
  • + * + * + * + * Now you could generate the entire file in Objective-C, + * but this would be a horribly tedious process. + * Beside, you want to design the file with professional tools to make it look pretty. + * + * So all you have to do is escape your dynamic content like this: + * + * ... + *

    %%ComputerName%% Control Panel

    + * ... + *
  • System Time: %%SysTime%%
  • + * + * And then you create an instance of this class with: + * + * - separator = @"%%" + * - replacementDictionary = { "ComputerName"="Black MacBook", "SysTime"="2010-04-30 03:18:24" } + * + * This class will then perform the replacements for you, on the fly, as it reads the file data. + * This class is also asynchronous, so it will perform the file IO using its own GCD queue. + * + * All keys for the replacementDictionary must be NSString's. + * Values for the replacementDictionary may be NSString's, or any object that + * returns what you want when its description method is invoked. +**/ + +@interface HTTPDynamicFileResponse : HTTPAsyncFileResponse +{ + NSData *separator; + NSDictionary *replacementDict; +} + +- (id)initWithFilePath:(NSString *)filePath + forConnection:(HTTPConnection *)connection + separator:(NSString *)separatorStr + replacementDictionary:(NSDictionary *)dictionary; + +@end diff --git a/msext/Class/http/Core/Responses/HTTPDynamicFileResponse.m b/msext/Class/http/Core/Responses/HTTPDynamicFileResponse.m new file mode 100755 index 0000000..e55426a --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPDynamicFileResponse.m @@ -0,0 +1,292 @@ +#import "HTTPDynamicFileResponse.h" +#import "HTTPConnection.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels : off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; + +#define NULL_FD -1 + + +@implementation HTTPDynamicFileResponse + +- (id)initWithFilePath:(NSString *)fpath + forConnection:(HTTPConnection *)parent + separator:(NSString *)separatorStr + replacementDictionary:(NSDictionary *)dict +{ + if ((self = [super initWithFilePath:fpath forConnection:parent])) + { + HTTPLogTrace(); + + separator = [separatorStr dataUsingEncoding:NSUTF8StringEncoding]; + replacementDict = dict; + } + return self; +} + +- (BOOL)isChunked +{ + HTTPLogTrace(); + + return YES; +} + +- (UInt64)contentLength +{ + // This method shouldn't be called since we're using a chunked response. + // We override it just to be safe. + + HTTPLogTrace(); + + return 0; +} + +- (void)setOffset:(UInt64)offset +{ + // This method shouldn't be called since we're using a chunked response. + // We override it just to be safe. + + HTTPLogTrace(); +} + +- (BOOL)isDone +{ + BOOL result = (readOffset == fileLength) && (readBufferOffset == 0); + + HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); + + return result; +} + +- (void)processReadBuffer +{ + HTTPLogTrace(); + + // At this point, the readBuffer has readBufferOffset bytes available. + // This method is in charge of updating the readBufferOffset. + + NSUInteger bufLen = readBufferOffset; + NSUInteger sepLen = [separator length]; + + // We're going to start looking for the separator at the beginning of the buffer, + // and stop when we get to the point where the separator would no longer fit in the buffer. + + NSUInteger offset = 0; + NSUInteger stopOffset = (bufLen > sepLen) ? bufLen - sepLen + 1 : 0; + + // In order to do the replacement, we need to find the starting and ending separator. + // For example: + // + // %%USER_NAME%% + // + // Where "%%" is the separator. + + BOOL found1 = NO; + BOOL found2 = NO; + + NSUInteger s1 = 0; + NSUInteger s2 = 0; + + const void *sep = [separator bytes]; + + while (offset < stopOffset) + { + const void *subBuffer = readBuffer + offset; + + if (memcmp(subBuffer, sep, sepLen) == 0) + { + if (!found1) + { + // Found the first separator + + found1 = YES; + s1 = offset; + offset += sepLen; + + HTTPLogVerbose(@"%@[%p]: Found s1 at %lu", THIS_FILE, self, (unsigned long)s1); + } + else + { + // Found the second separator + + found2 = YES; + s2 = offset; + offset += sepLen; + + HTTPLogVerbose(@"%@[%p]: Found s2 at %lu", THIS_FILE, self, (unsigned long)s2); + } + + if (found1 && found2) + { + // We found our separators. + // Now extract the string between the two separators. + + NSRange fullRange = NSMakeRange(s1, (s2 - s1 + sepLen)); + NSRange strRange = NSMakeRange(s1 + sepLen, (s2 - s1 - sepLen)); + + // Wish we could use the simple subdataWithRange method. + // But that method copies the bytes... + // So for performance reasons, we need to use the methods that don't copy the bytes. + + void *strBuf = readBuffer + strRange.location; + NSUInteger strLen = strRange.length; + + NSString *key = [[NSString alloc] initWithBytes:strBuf length:strLen encoding:NSUTF8StringEncoding]; + if (key) + { + // Is there a given replacement for this key? + + id value = [replacementDict objectForKey:key]; + if (value) + { + // Found the replacement value. + // Now perform the replacement in the buffer. + + HTTPLogVerbose(@"%@[%p]: key(%@) -> value(%@)", THIS_FILE, self, key, value); + + NSData *v = [[value description] dataUsingEncoding:NSUTF8StringEncoding]; + NSUInteger vLength = [v length]; + + if (fullRange.length == vLength) + { + // Replacement is exactly the same size as what it is replacing + + // memcpy(void *restrict dst, const void *restrict src, size_t n); + + memcpy(readBuffer + fullRange.location, [v bytes], vLength); + } + else // (fullRange.length != vLength) + { + NSInteger diff = (NSInteger)vLength - (NSInteger)fullRange.length; + + if (diff > 0) + { + // Replacement is bigger than what it is replacing. + // Make sure there is room in the buffer for the replacement. + + if (diff > (readBufferSize - bufLen)) + { + NSUInteger inc = MAX(diff, 256); + + readBufferSize += inc; + readBuffer = reallocf(readBuffer, readBufferSize); + } + } + + // Move the data that comes after the replacement. + // + // If replacement is smaller than what it is replacing, + // then we are shifting the data toward the beginning of the buffer. + // + // If replacement is bigger than what it is replacing, + // then we are shifting the data toward the end of the buffer. + // + // memmove(void *dst, const void *src, size_t n); + // + // The memmove() function copies n bytes from src to dst. + // The two areas may overlap; the copy is always done in a non-destructive manner. + + void *src = readBuffer + fullRange.location + fullRange.length; + void *dst = readBuffer + fullRange.location + vLength; + + NSUInteger remaining = bufLen - (fullRange.location + fullRange.length); + + memmove(dst, src, remaining); + + // Now copy the replacement into its location. + // + // memcpy(void *restrict dst, const void *restrict src, size_t n) + // + // The memcpy() function copies n bytes from src to dst. + // If the two areas overlap, behavior is undefined. + + memcpy(readBuffer + fullRange.location, [v bytes], vLength); + + // And don't forget to update our indices. + + bufLen += diff; + offset += diff; + stopOffset += diff; + } + } + + } + + found1 = found2 = NO; + } + } + else + { + offset++; + } + } + + // We've gone through our buffer now, and performed all the replacements that we could. + // It's now time to update the amount of available data we have. + + if (readOffset == fileLength) + { + // We've read in the entire file. + // So there can be no more replacements. + + data = [[NSData alloc] initWithBytes:readBuffer length:bufLen]; + readBufferOffset = 0; + } + else + { + // There are a couple different situations that we need to take into account here. + // + // Imagine the following file: + // My name is %%USER_NAME%% + // + // Situation 1: + // The first chunk of data we read was "My name is %%". + // So we found the first separator, but not the second. + // In this case we can only return the data that precedes the first separator. + // + // Situation 2: + // The first chunk of data we read was "My name is %". + // So we didn't find any separators, but part of a separator may be included in our buffer. + + NSUInteger available; + if (found1) + { + // Situation 1 + available = s1; + } + else + { + // Situation 2 + available = stopOffset; + } + + // Copy available data + + data = [[NSData alloc] initWithBytes:readBuffer length:available]; + + // Remove the copied data from the buffer. + // We do this by shifting the remaining data toward the beginning of the buffer. + + NSUInteger remaining = bufLen - available; + + memmove(readBuffer, readBuffer + available, remaining); + readBufferOffset = remaining; + } + + [connection responseHasAvailableData:self]; +} + +- (void)dealloc +{ + HTTPLogTrace(); + + +} + +@end diff --git a/msext/Class/http/Core/Responses/HTTPErrorResponse.h b/msext/Class/http/Core/Responses/HTTPErrorResponse.h new file mode 100755 index 0000000..7aa9c7e --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPErrorResponse.h @@ -0,0 +1,9 @@ +#import "HTTPResponse.h" + +@interface HTTPErrorResponse : NSObject { + NSInteger _status; +} + +- (id)initWithErrorCode:(int)httpErrorCode; + +@end diff --git a/msext/Class/http/Core/Responses/HTTPErrorResponse.m b/msext/Class/http/Core/Responses/HTTPErrorResponse.m new file mode 100755 index 0000000..02871ae --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPErrorResponse.m @@ -0,0 +1,38 @@ +#import "HTTPErrorResponse.h" + +@implementation HTTPErrorResponse + +-(id)initWithErrorCode:(int)httpErrorCode +{ + if ((self = [super init])) + { + _status = httpErrorCode; + } + + return self; +} + +- (UInt64) contentLength { + return 0; +} + +- (UInt64) offset { + return 0; +} + +- (void)setOffset:(UInt64)offset { + ; +} + +- (NSData*) readDataOfLength:(NSUInteger)length { + return nil; +} + +- (BOOL) isDone { + return YES; +} + +- (NSInteger) status { + return _status; +} +@end diff --git a/msext/Class/http/Core/Responses/HTTPFileResponse.h b/msext/Class/http/Core/Responses/HTTPFileResponse.h new file mode 100755 index 0000000..e334f4f --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPFileResponse.h @@ -0,0 +1,25 @@ +#import +#import "HTTPResponse.h" + +@class HTTPConnection; + + +@interface HTTPFileResponse : NSObject +{ + HTTPConnection *connection; + + NSString *filePath; + UInt64 fileLength; + UInt64 fileOffset; + + BOOL aborted; + + int fileFD; + void *buffer; + NSUInteger bufferSize; +} + +- (id)initWithFilePath:(NSString *)filePath forConnection:(HTTPConnection *)connection; +- (NSString *)filePath; + +@end diff --git a/msext/Class/http/Core/Responses/HTTPFileResponse.m b/msext/Class/http/Core/Responses/HTTPFileResponse.m new file mode 100755 index 0000000..cf73cd2 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPFileResponse.m @@ -0,0 +1,237 @@ +#import "HTTPFileResponse.h" +#import "HTTPConnection.h" +#import "HTTPLogging.h" + +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels : off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; + +#define NULL_FD -1 + + +@implementation HTTPFileResponse + +- (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent +{ + if((self = [super init])) + { + HTTPLogTrace(); + + connection = parent; // Parents retain children, children do NOT retain parents + + fileFD = NULL_FD; + filePath = [[fpath copy] stringByResolvingSymlinksInPath]; + if (filePath == nil) + { + HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE); + + return nil; + } + + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; + if (fileAttributes == nil) + { + HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath); + + return nil; + } + + fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue]; + fileOffset = 0; + + aborted = NO; + + // We don't bother opening the file here. + // If this is a HEAD request we only need to know the fileLength. + } + return self; +} + +- (void)abort +{ + HTTPLogTrace(); + + [connection responseDidAbort:self]; + aborted = YES; +} + +- (BOOL)openFile +{ + HTTPLogTrace(); + + fileFD = open([filePath UTF8String], O_RDONLY); + if (fileFD == NULL_FD) + { + HTTPLogError(@"%@[%p]: Unable to open file. filePath: %@", THIS_FILE, self, filePath); + + [self abort]; + return NO; + } + + HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath); + + return YES; +} + +- (BOOL)openFileIfNeeded +{ + if (aborted) + { + // The file operation has been aborted. + // This could be because we failed to open the file, + // or the reading process failed. + return NO; + } + + if (fileFD != NULL_FD) + { + // File has already been opened. + return YES; + } + + return [self openFile]; +} + +- (UInt64)contentLength +{ + HTTPLogTrace(); + + return fileLength; +} + +- (UInt64)offset +{ + HTTPLogTrace(); + + return fileOffset; +} + +- (void)setOffset:(UInt64)offset +{ + HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset); + + if (![self openFileIfNeeded]) + { + // File opening failed, + // or response has been aborted due to another error. + return; + } + + fileOffset = offset; + + off_t result = lseek(fileFD, (off_t)offset, SEEK_SET); + if (result == -1) + { + HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath); + + [self abort]; + } +} + +- (NSData *)readDataOfLength:(NSUInteger)length +{ + HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length); + + if (![self openFileIfNeeded]) + { + // File opening failed, + // or response has been aborted due to another error. + return nil; + } + + // Determine how much data we should read. + // + // It is OK if we ask to read more bytes than exist in the file. + // It is NOT OK to over-allocate the buffer. + + UInt64 bytesLeftInFile = fileLength - fileOffset; + + NSUInteger bytesToRead = (NSUInteger)MIN(length, bytesLeftInFile); + + // Make sure buffer is big enough for read request. + // Do not over-allocate. + + if (buffer == NULL || bufferSize < bytesToRead) + { + bufferSize = bytesToRead; + buffer = reallocf(buffer, (size_t)bufferSize); + + if (buffer == NULL) + { + HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self); + + [self abort]; + return nil; + } + } + + // Perform the read + + HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead); + + ssize_t result = read(fileFD, buffer, bytesToRead); + + // Check the results + + if (result < 0) + { + HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath); + + [self abort]; + return nil; + } + else if (result == 0) + { + HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath); + + [self abort]; + return nil; + } + else // (result > 0) + { + HTTPLogVerbose(@"%@[%p]: Read %ld bytes from file", THIS_FILE, self, (long)result); + + fileOffset += result; + + return [NSData dataWithBytes:buffer length:result]; + } +} + +- (BOOL)isDone +{ + BOOL result = (fileOffset == fileLength); + + HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); + + return result; +} + +- (NSString *)filePath +{ + return filePath; +} + +- (void)dealloc +{ + HTTPLogTrace(); + + if (fileFD != NULL_FD) + { + HTTPLogVerbose(@"%@[%p]: Close fd[%i]", THIS_FILE, self, fileFD); + + close(fileFD); + } + + if (buffer) + free(buffer); + +} + +@end diff --git a/msext/Class/http/Core/Responses/HTTPRedirectResponse.h b/msext/Class/http/Core/Responses/HTTPRedirectResponse.h new file mode 100755 index 0000000..1e90123 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPRedirectResponse.h @@ -0,0 +1,12 @@ +#import +#import "HTTPResponse.h" + + +@interface HTTPRedirectResponse : NSObject +{ + NSString *redirectPath; +} + +- (id)initWithPath:(NSString *)redirectPath; + +@end diff --git a/msext/Class/http/Core/Responses/HTTPRedirectResponse.m b/msext/Class/http/Core/Responses/HTTPRedirectResponse.m new file mode 100755 index 0000000..1fc7020 --- /dev/null +++ b/msext/Class/http/Core/Responses/HTTPRedirectResponse.m @@ -0,0 +1,73 @@ +#import "HTTPRedirectResponse.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels : off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_OFF; // | HTTP_LOG_FLAG_TRACE; + + +@implementation HTTPRedirectResponse + +- (id)initWithPath:(NSString *)path +{ + if ((self = [super init])) + { + HTTPLogTrace(); + + redirectPath = [path copy]; + } + return self; +} + +- (UInt64)contentLength +{ + return 0; +} + +- (UInt64)offset +{ + return 0; +} + +- (void)setOffset:(UInt64)offset +{ + // Nothing to do +} + +- (NSData *)readDataOfLength:(NSUInteger)length +{ + HTTPLogTrace(); + + return nil; +} + +- (BOOL)isDone +{ + return YES; +} + +- (NSDictionary *)httpHeaders +{ + HTTPLogTrace(); + + return [NSDictionary dictionaryWithObject:redirectPath forKey:@"Location"]; +} + +- (NSInteger)status +{ + HTTPLogTrace(); + + return 302; +} + +- (void)dealloc +{ + HTTPLogTrace(); + +} + +@end diff --git a/msext/Class/http/Core/WebSocket.h b/msext/Class/http/Core/WebSocket.h new file mode 100755 index 0000000..4ee5137 --- /dev/null +++ b/msext/Class/http/Core/WebSocket.h @@ -0,0 +1,105 @@ +#import + +@class HTTPMessage; +@class GCDAsyncSocket; + + +#define WebSocketDidDieNotification @"WebSocketDidDie" + +@interface WebSocket : NSObject +{ + dispatch_queue_t websocketQueue; + + HTTPMessage *request; + GCDAsyncSocket *asyncSocket; + + NSData *term; + + BOOL isStarted; + BOOL isOpen; + BOOL isVersion76; + + id __unsafe_unretained delegate; +} + ++ (BOOL)isWebSocketRequest:(HTTPMessage *)request; + +- (id)initWithRequest:(HTTPMessage *)request socket:(GCDAsyncSocket *)socket; + +/** + * Delegate option. + * + * In most cases it will be easier to subclass WebSocket, + * but some circumstances may lead one to prefer standard delegate callbacks instead. +**/ +@property (/* atomic */ unsafe_unretained) id delegate; + +/** + * The WebSocket class is thread-safe, generally via it's GCD queue. + * All public API methods are thread-safe, + * and the subclass API methods are thread-safe as they are all invoked on the same GCD queue. +**/ +@property (nonatomic, readonly) dispatch_queue_t websocketQueue; + +/** + * Public API + * + * These methods are automatically called by the HTTPServer. + * You may invoke the stop method yourself to close the WebSocket manually. +**/ +- (void)start; +- (void)stop; + +/** + * Public API + * + * Sends a message over the WebSocket. + * This method is thread-safe. + **/ +- (void)sendMessage:(NSString *)msg; + +/** + * Public API + * + * Sends a message over the WebSocket. + * This method is thread-safe. + **/ +- (void)sendData:(NSData *)msg; + +/** + * Subclass API + * + * These methods are designed to be overriden by subclasses. +**/ +- (void)didOpen; +- (void)didReceiveMessage:(NSString *)msg; +- (void)didClose; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * There are two ways to create your own custom WebSocket: + * + * - Subclass it and override the methods you're interested in. + * - Use traditional delegate paradigm along with your own custom class. + * + * They both exist to allow for maximum flexibility. + * In most cases it will be easier to subclass WebSocket. + * However some circumstances may lead one to prefer standard delegate callbacks instead. + * One such example, you're already subclassing another class, so subclassing WebSocket isn't an option. +**/ + +@protocol WebSocketDelegate +@optional + +- (void)webSocketDidOpen:(WebSocket *)ws; + +- (void)webSocket:(WebSocket *)ws didReceiveMessage:(NSString *)msg; + +- (void)webSocketDidClose:(WebSocket *)ws; + +@end diff --git a/msext/Class/http/Core/WebSocket.m b/msext/Class/http/Core/WebSocket.m new file mode 100755 index 0000000..9051ac3 --- /dev/null +++ b/msext/Class/http/Core/WebSocket.m @@ -0,0 +1,791 @@ +#import "WebSocket.h" +#import "HTTPMessage.h" +#import "GCDAsyncSocket.h" +#import "DDNumber.h" +#import "DDData.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Other flags : trace +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; + +#define TIMEOUT_NONE -1 +#define TIMEOUT_REQUEST_BODY 10 + +#define TAG_HTTP_REQUEST_BODY 100 +#define TAG_HTTP_RESPONSE_HEADERS 200 +#define TAG_HTTP_RESPONSE_BODY 201 + +#define TAG_PREFIX 300 +#define TAG_MSG_PLUS_SUFFIX 301 +#define TAG_MSG_WITH_LENGTH 302 +#define TAG_MSG_MASKING_KEY 303 +#define TAG_PAYLOAD_PREFIX 304 +#define TAG_PAYLOAD_LENGTH 305 +#define TAG_PAYLOAD_LENGTH16 306 +#define TAG_PAYLOAD_LENGTH64 307 + +#define WS_OP_CONTINUATION_FRAME 0 +#define WS_OP_TEXT_FRAME 1 +#define WS_OP_BINARY_FRAME 2 +#define WS_OP_CONNECTION_CLOSE 8 +#define WS_OP_PING 9 +#define WS_OP_PONG 10 + +static inline BOOL WS_OP_IS_FINAL_FRAGMENT(UInt8 frame) +{ + return (frame & 0x80) ? YES : NO; +} + +static inline BOOL WS_PAYLOAD_IS_MASKED(UInt8 frame) +{ + return (frame & 0x80) ? YES : NO; +} + +static inline NSUInteger WS_PAYLOAD_LENGTH(UInt8 frame) +{ + return frame & 0x7F; +} + +@interface WebSocket (PrivateAPI) + +- (void)readRequestBody; +- (void)sendResponseBody; +- (void)sendResponseHeaders; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation WebSocket +{ + BOOL isRFC6455; + BOOL nextFrameMasked; + NSUInteger nextOpCode; + NSData *maskingKey; +} + ++ (BOOL)isWebSocketRequest:(HTTPMessage *)request +{ + // Request (Draft 75): + // + // GET /demo HTTP/1.1 + // Upgrade: WebSocket + // Connection: Upgrade + // Host: example.com + // Origin: http://example.com + // WebSocket-Protocol: sample + // + // + // Request (Draft 76): + // + // GET /demo HTTP/1.1 + // Upgrade: WebSocket + // Connection: Upgrade + // Host: example.com + // Origin: http://example.com + // Sec-WebSocket-Protocol: sample + // Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5 + // Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 + // + // ^n:ds[4U + + // Look for Upgrade: and Connection: headers. + // If we find them, and they have the proper value, + // we can safely assume this is a websocket request. + + NSString *upgradeHeaderValue = [request headerField:@"Upgrade"]; + NSString *connectionHeaderValue = [request headerField:@"Connection"]; + + BOOL isWebSocket = YES; + + if (!upgradeHeaderValue || !connectionHeaderValue) { + isWebSocket = NO; + } + else if (![upgradeHeaderValue caseInsensitiveCompare:@"WebSocket"] == NSOrderedSame) { + isWebSocket = NO; + } + else if ([connectionHeaderValue rangeOfString:@"Upgrade" options:NSCaseInsensitiveSearch].location == NSNotFound) { + isWebSocket = NO; + } + + HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isWebSocket ? @"YES" : @"NO")); + + return isWebSocket; +} + ++ (BOOL)isVersion76Request:(HTTPMessage *)request +{ + NSString *key1 = [request headerField:@"Sec-WebSocket-Key1"]; + NSString *key2 = [request headerField:@"Sec-WebSocket-Key2"]; + + BOOL isVersion76; + + if (!key1 || !key2) { + isVersion76 = NO; + } + else { + isVersion76 = YES; + } + + HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isVersion76 ? @"YES" : @"NO")); + + return isVersion76; +} + ++ (BOOL)isRFC6455Request:(HTTPMessage *)request +{ + NSString *key = [request headerField:@"Sec-WebSocket-Key"]; + BOOL isRFC6455 = (key != nil); + + HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isRFC6455 ? @"YES" : @"NO")); + + return isRFC6455; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Setup and Teardown +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize websocketQueue; + +- (id)initWithRequest:(HTTPMessage *)aRequest socket:(GCDAsyncSocket *)socket +{ + HTTPLogTrace(); + + if (aRequest == nil) + { + return nil; + } + + if ((self = [super init])) + { + if (HTTP_LOG_VERBOSE) + { + NSData *requestHeaders = [aRequest messageData]; + + NSString *temp = [[NSString alloc] initWithData:requestHeaders encoding:NSUTF8StringEncoding]; + HTTPLogVerbose(@"%@[%p] Request Headers:\n%@", THIS_FILE, self, temp); + } + + websocketQueue = dispatch_queue_create("WebSocket", NULL); + request = aRequest; + + asyncSocket = socket; + [asyncSocket setDelegate:self delegateQueue:websocketQueue]; + + isOpen = NO; + isVersion76 = [[self class] isVersion76Request:request]; + isRFC6455 = [[self class] isRFC6455Request:request]; + + term = [[NSData alloc] initWithBytes:"\xFF" length:1]; + } + return self; +} + +- (void)dealloc +{ + HTTPLogTrace(); + + #if !OS_OBJECT_USE_OBJC + dispatch_release(websocketQueue); + #endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; + [asyncSocket disconnect]; +} + +- (id)delegate +{ + __block id result = nil; + + dispatch_sync(websocketQueue, ^{ + result = delegate; + }); + + return result; +} + +- (void)setDelegate:(id)newDelegate +{ + dispatch_async(websocketQueue, ^{ + delegate = newDelegate; + }); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Start and Stop +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Starting point for the WebSocket after it has been fully initialized (including subclasses). + * This method is called by the HTTPConnection it is spawned from. +**/ +- (void)start +{ + // This method is not exactly designed to be overriden. + // Subclasses are encouraged to override the didOpen method instead. + + dispatch_async(websocketQueue, ^{ @autoreleasepool { + + if (isStarted) return; + isStarted = YES; + + if (isVersion76) + { + [self readRequestBody]; + } + else + { + [self sendResponseHeaders]; + [self didOpen]; + } + }}); +} + +/** + * This method is called by the HTTPServer if it is asked to stop. + * The server, in turn, invokes stop on each WebSocket instance. +**/ +- (void)stop +{ + // This method is not exactly designed to be overriden. + // Subclasses are encouraged to override the didClose method instead. + + dispatch_async(websocketQueue, ^{ @autoreleasepool { + + [asyncSocket disconnect]; + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark HTTP Response +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readRequestBody +{ + HTTPLogTrace(); + + NSAssert(isVersion76, @"WebSocket version 75 doesn't contain a request body"); + + [asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_HTTP_REQUEST_BODY]; +} + +- (NSString *)originResponseHeaderValue +{ + HTTPLogTrace(); + + NSString *origin = [request headerField:@"Origin"]; + + if (origin == nil) + { + NSString *port = [NSString stringWithFormat:@"%hu", [asyncSocket localPort]]; + + return [NSString stringWithFormat:@"http://localhost:%@", port]; + } + else + { + return origin; + } +} + +- (NSString *)locationResponseHeaderValue +{ + HTTPLogTrace(); + + NSString *location; + + NSString *scheme = [asyncSocket isSecure] ? @"wss" : @"ws"; + NSString *host = [request headerField:@"Host"]; + + NSString *requestUri = [[request url] relativeString]; + + if (host == nil) + { + NSString *port = [NSString stringWithFormat:@"%hu", [asyncSocket localPort]]; + + location = [NSString stringWithFormat:@"%@://localhost:%@%@", scheme, port, requestUri]; + } + else + { + location = [NSString stringWithFormat:@"%@://%@%@", scheme, host, requestUri]; + } + + return location; +} + +- (NSString *)secWebSocketKeyResponseHeaderValue { + NSString *key = [request headerField: @"Sec-WebSocket-Key"]; + NSString *guid = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + return [[key stringByAppendingString: guid] dataUsingEncoding: NSUTF8StringEncoding].sha1Digest.base64Encoded; +} + +- (void)sendResponseHeaders +{ + HTTPLogTrace(); + + // Request (Draft 75): + // + // GET /demo HTTP/1.1 + // Upgrade: WebSocket + // Connection: Upgrade + // Host: example.com + // Origin: http://example.com + // WebSocket-Protocol: sample + // + // + // Request (Draft 76): + // + // GET /demo HTTP/1.1 + // Upgrade: WebSocket + // Connection: Upgrade + // Host: example.com + // Origin: http://example.com + // Sec-WebSocket-Protocol: sample + // Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 + // Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5 + // + // ^n:ds[4U + + + // Response (Draft 75): + // + // HTTP/1.1 101 Web Socket Protocol Handshake + // Upgrade: WebSocket + // Connection: Upgrade + // WebSocket-Origin: http://example.com + // WebSocket-Location: ws://example.com/demo + // WebSocket-Protocol: sample + // + // + // Response (Draft 76): + // + // HTTP/1.1 101 WebSocket Protocol Handshake + // Upgrade: WebSocket + // Connection: Upgrade + // Sec-WebSocket-Origin: http://example.com + // Sec-WebSocket-Location: ws://example.com/demo + // Sec-WebSocket-Protocol: sample + // + // 8jKS'y:G*Co,Wxa- + + + HTTPMessage *wsResponse = [[HTTPMessage alloc] initResponseWithStatusCode:101 + description:@"Web Socket Protocol Handshake" + version:HTTPVersion1_1]; + + [wsResponse setHeaderField:@"Upgrade" value:@"WebSocket"]; + [wsResponse setHeaderField:@"Connection" value:@"Upgrade"]; + + // Note: It appears that WebSocket-Origin and WebSocket-Location + // are required for Google's Chrome implementation to work properly. + // + // If we don't send either header, Chrome will never report the WebSocket as open. + // If we only send one of the two, Chrome will immediately close the WebSocket. + // + // In addition to this it appears that Chrome's implementation is very picky of the values of the headers. + // They have to match exactly with what Chrome sent us or it will close the WebSocket. + + NSString *originValue = [self originResponseHeaderValue]; + NSString *locationValue = [self locationResponseHeaderValue]; + + NSString *originField = isVersion76 ? @"Sec-WebSocket-Origin" : @"WebSocket-Origin"; + NSString *locationField = isVersion76 ? @"Sec-WebSocket-Location" : @"WebSocket-Location"; + + [wsResponse setHeaderField:originField value:originValue]; + [wsResponse setHeaderField:locationField value:locationValue]; + + NSString *acceptValue = [self secWebSocketKeyResponseHeaderValue]; + if (acceptValue) { + [wsResponse setHeaderField: @"Sec-WebSocket-Accept" value: acceptValue]; + } + + NSData *responseHeaders = [wsResponse messageData]; + + + if (HTTP_LOG_VERBOSE) + { + NSString *temp = [[NSString alloc] initWithData:responseHeaders encoding:NSUTF8StringEncoding]; + HTTPLogVerbose(@"%@[%p] Response Headers:\n%@", THIS_FILE, self, temp); + } + + [asyncSocket writeData:responseHeaders withTimeout:TIMEOUT_NONE tag:TAG_HTTP_RESPONSE_HEADERS]; +} + +- (NSData *)processKey:(NSString *)key +{ + HTTPLogTrace(); + + unichar c; + NSUInteger i; + NSUInteger length = [key length]; + + // Concatenate the digits into a string, + // and count the number of spaces. + + NSMutableString *numStr = [NSMutableString stringWithCapacity:10]; + long long numSpaces = 0; + + for (i = 0; i < length; i++) + { + c = [key characterAtIndex:i]; + + if (c >= '0' && c <= '9') + { + [numStr appendFormat:@"%C", c]; + } + else if (c == ' ') + { + numSpaces++; + } + } + + long long num = strtoll([numStr UTF8String], NULL, 10); + + long long resultHostNum; + + if (numSpaces == 0) + resultHostNum = 0; + else + resultHostNum = num / numSpaces; + + HTTPLogVerbose(@"key(%@) -> %qi / %qi = %qi", key, num, numSpaces, resultHostNum); + + // Convert result to 4 byte big-endian (network byte order) + // and then convert to raw data. + + UInt32 result = OSSwapHostToBigInt32((uint32_t)resultHostNum); + + return [NSData dataWithBytes:&result length:4]; +} + +- (void)sendResponseBody:(NSData *)d3 +{ + HTTPLogTrace(); + + NSAssert(isVersion76, @"WebSocket version 75 doesn't contain a response body"); + NSAssert([d3 length] == 8, @"Invalid requestBody length"); + + NSString *key1 = [request headerField:@"Sec-WebSocket-Key1"]; + NSString *key2 = [request headerField:@"Sec-WebSocket-Key2"]; + + NSData *d1 = [self processKey:key1]; + NSData *d2 = [self processKey:key2]; + + // Concatenated d1, d2 & d3 + + NSMutableData *d0 = [NSMutableData dataWithCapacity:(4+4+8)]; + [d0 appendData:d1]; + [d0 appendData:d2]; + [d0 appendData:d3]; + + // Hash the data using MD5 + + NSData *responseBody = [d0 md5Digest]; + + [asyncSocket writeData:responseBody withTimeout:TIMEOUT_NONE tag:TAG_HTTP_RESPONSE_BODY]; + + if (HTTP_LOG_VERBOSE) + { + NSString *s1 = [[NSString alloc] initWithData:d1 encoding:NSASCIIStringEncoding]; + NSString *s2 = [[NSString alloc] initWithData:d2 encoding:NSASCIIStringEncoding]; + NSString *s3 = [[NSString alloc] initWithData:d3 encoding:NSASCIIStringEncoding]; + + NSString *s0 = [[NSString alloc] initWithData:d0 encoding:NSASCIIStringEncoding]; + + NSString *sH = [[NSString alloc] initWithData:responseBody encoding:NSASCIIStringEncoding]; + + HTTPLogVerbose(@"key1 result : raw(%@) str(%@)", d1, s1); + HTTPLogVerbose(@"key2 result : raw(%@) str(%@)", d2, s2); + HTTPLogVerbose(@"key3 passed : raw(%@) str(%@)", d3, s3); + HTTPLogVerbose(@"key0 concat : raw(%@) str(%@)", d0, s0); + HTTPLogVerbose(@"responseBody: raw(%@) str(%@)", responseBody, sH); + + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Core Functionality +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didOpen +{ + HTTPLogTrace(); + + // Override me to perform any custom actions once the WebSocket has been opened. + // This method is invoked on the websocketQueue. + // + // Don't forget to invoke [super didOpen] in your method. + + // Start reading for messages + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:(isRFC6455 ? TAG_PAYLOAD_PREFIX : TAG_PREFIX)]; + + // Notify delegate + if ([delegate respondsToSelector:@selector(webSocketDidOpen:)]) + { + [delegate webSocketDidOpen:self]; + } +} + +- (void)sendMessage:(NSString *)msg +{ + NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding]; + [self sendData:msgData]; +} + +- (void)sendData:(NSData *)msgData +{ + HTTPLogTrace(); + + NSMutableData *data = nil; + + if (isRFC6455) + { + NSUInteger length = msgData.length; + if (length <= 125) + { + data = [NSMutableData dataWithCapacity:(length + 2)]; + [data appendBytes: "\x81" length:1]; + UInt8 len = (UInt8)length; + [data appendBytes: &len length:1]; + [data appendData:msgData]; + } + else if (length <= 0xFFFF) + { + data = [NSMutableData dataWithCapacity:(length + 4)]; + [data appendBytes: "\x81\x7E" length:2]; + UInt16 len = (UInt16)length; + [data appendBytes: (UInt8[]){len >> 8, len & 0xFF} length:2]; + [data appendData:msgData]; + } + else + { + data = [NSMutableData dataWithCapacity:(length + 10)]; + [data appendBytes: "\x81\x7F" length:2]; + [data appendBytes: (UInt8[]){0, 0, 0, 0, (UInt8)(length >> 24), (UInt8)(length >> 16), (UInt8)(length >> 8), length & 0xFF} length:8]; + [data appendData:msgData]; + } + } + else + { + data = [NSMutableData dataWithCapacity:([msgData length] + 2)]; + + [data appendBytes:"\x00" length:1]; + [data appendData:msgData]; + [data appendBytes:"\xFF" length:1]; + } + + // Remember: GCDAsyncSocket is thread-safe + + [asyncSocket writeData:data withTimeout:TIMEOUT_NONE tag:0]; +} + +- (void)didReceiveMessage:(NSString *)msg +{ + HTTPLogTrace(); + + // Override me to process incoming messages. + // This method is invoked on the websocketQueue. + // + // For completeness, you should invoke [super didReceiveMessage:msg] in your method. + + // Notify delegate + if ([delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)]) + { + [delegate webSocket:self didReceiveMessage:msg]; + } +} + +- (void)didClose +{ + HTTPLogTrace(); + + // Override me to perform any cleanup when the socket is closed + // This method is invoked on the websocketQueue. + // + // Don't forget to invoke [super didClose] at the end of your method. + + // Notify delegate + if ([delegate respondsToSelector:@selector(webSocketDidClose:)]) + { + [delegate webSocketDidClose:self]; + } + + // Notify HTTPServer + [[NSNotificationCenter defaultCenter] postNotificationName:WebSocketDidDieNotification object:self]; +} + +#pragma mark WebSocket Frame + +- (BOOL)isValidWebSocketFrame:(UInt8)frame +{ + NSUInteger rsv = frame & 0x70; + NSUInteger opcode = frame & 0x0F; + if (rsv || (3 <= opcode && opcode <= 7) || (0xB <= opcode && opcode <= 0xF)) + { + return NO; + } + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark AsyncSocket Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-------+-+-------------+-------------------------------+ +// |F|R|R|R| opcode|M| Payload len | Extended payload length | +// |I|S|S|S| (4) |A| (7) | (16/64) | +// |N|V|V|V| |S| | (if payload len==126/127) | +// | |1|2|3| |K| | | +// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +// | Extended payload length continued, if payload len == 127 | +// + - - - - - - - - - - - - - - - +-------------------------------+ +// | |Masking-key, if MASK set to 1 | +// +-------------------------------+-------------------------------+ +// | Masking-key (continued) | Payload Data | +// +-------------------------------- - - - - - - - - - - - - - - - + +// : Payload Data continued ... : +// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// | Payload Data continued ... | +// +---------------------------------------------------------------+ + +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag +{ + HTTPLogTrace(); + + if (tag == TAG_HTTP_REQUEST_BODY) + { + [self sendResponseHeaders]; + [self sendResponseBody:data]; + [self didOpen]; + } + else if (tag == TAG_PREFIX) + { + UInt8 *pFrame = (UInt8 *)[data bytes]; + UInt8 frame = *pFrame; + + if (frame <= 0x7F) + { + [asyncSocket readDataToData:term withTimeout:TIMEOUT_NONE tag:TAG_MSG_PLUS_SUFFIX]; + } + else + { + // Unsupported frame type + [self didClose]; + } + } + else if (tag == TAG_PAYLOAD_PREFIX) + { + UInt8 *pFrame = (UInt8 *)[data bytes]; + UInt8 frame = *pFrame; + + if ([self isValidWebSocketFrame: frame]) + { + nextOpCode = (frame & 0x0F); + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH]; + } + else + { + // Unsupported frame type + [self didClose]; + } + } + else if (tag == TAG_PAYLOAD_LENGTH) + { + UInt8 frame = *(UInt8 *)[data bytes]; + BOOL masked = WS_PAYLOAD_IS_MASKED(frame); + NSUInteger length = WS_PAYLOAD_LENGTH(frame); + nextFrameMasked = masked; + maskingKey = nil; + if (length <= 125) + { + if (nextFrameMasked) + { + [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; + } + [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_MSG_WITH_LENGTH]; + } + else if (length == 126) + { + [asyncSocket readDataToLength:2 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH16]; + } + else + { + [asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH64]; + } + } + else if (tag == TAG_PAYLOAD_LENGTH16) + { + UInt8 *pFrame = (UInt8 *)[data bytes]; + NSUInteger length = ((NSUInteger)pFrame[0] << 8) | (NSUInteger)pFrame[1]; + if (nextFrameMasked) { + [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; + } + [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_MSG_WITH_LENGTH]; + } + else if (tag == TAG_PAYLOAD_LENGTH64) + { + // FIXME: 64bit data size in memory? + [self didClose]; + } + else if (tag == TAG_MSG_WITH_LENGTH) + { + NSUInteger msgLength = [data length]; + if (nextFrameMasked && maskingKey) { + NSMutableData *masked = data.mutableCopy; + UInt8 *pData = (UInt8 *)masked.mutableBytes; + UInt8 *pMask = (UInt8 *)maskingKey.bytes; + for (NSUInteger i = 0; i < msgLength; i++) + { + pData[i] = pData[i] ^ pMask[i % 4]; + } + data = masked; + } + if (nextOpCode == WS_OP_TEXT_FRAME) + { + NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding]; + [self didReceiveMessage:msg]; + } + else + { + [self didClose]; + return; + } + + // Read next frame + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; + } + else if (tag == TAG_MSG_MASKING_KEY) + { + maskingKey = data.copy; + } + else + { + NSUInteger msgLength = [data length] - 1; // Excluding ending 0xFF frame + + NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding]; + + [self didReceiveMessage:msg]; + + + // Read next message + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PREFIX]; + } +} + +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error +{ + HTTPLogTrace2(@"%@[%p]: socketDidDisconnect:withError: %@", THIS_FILE, self, error); + + [self didClose]; +} + +@end diff --git a/msext/Class/libs/JANALYTICSEventObject.h b/msext/Class/libs/JANALYTICSEventObject.h new file mode 100755 index 0000000..f6aefef --- /dev/null +++ b/msext/Class/libs/JANALYTICSEventObject.h @@ -0,0 +1,174 @@ +/* + * | | | | \ \ / / | | | | / _______| + * | |____| | \ \/ / | |____| | / / + * | |____| | \ / | |____| | | | _____ + * | | | | / \ | | | | | | |____ | + * | | | | / /\ \ | | | | \ \______| | + * | | | | /_/ \_\ | | | | \_________| + * + * Copyright (c) 2016~ Shenzhen HXHG. All rights reserved. + */ + +#import +#import +/** + * + * @abstract 事件对象共同父类 + * + * @discussion 所有的字符串属性长度不能超过256字节(包括extra的key和value) + * + */ +@interface JANALYTICSEventObject : NSObject + +@property (nonatomic, strong, nonnull) NSDictionary* extra; + +@end + +/** + 登录事件对象 + */ +@interface JANALYTICSLoginEvent : JANALYTICSEventObject + +//登录方法,非空 +@property (nonatomic, copy, nonnull) NSString* method; +//登录是否成功,非空 +@property (nonatomic, assign) BOOL success; + +@end + +/** + 注册事件对象 + */ +@interface JANALYTICSRegisterEvent : JANALYTICSEventObject + +//注册方法,非空 +@property (nonatomic, copy, nonnull) NSString* method; +//注册是否成功,非空 +@property (nonatomic, assign) BOOL success; + +@end + +typedef NS_ENUM(NSUInteger, JANALYTICSPurchaseCurrency) { + //人民币 + JANALYTICSCurrencyCNY, + //美元 + JANALYTICSCurrencyUSD +}; +/** + 购买事件对象 + */ +@interface JANALYTICSPurchaseEvent : JANALYTICSEventObject + +//价格 非空 +@property (nonatomic, assign) CGFloat price; +//购买是否成功,非空 +@property (nonatomic, assign) BOOL success; +//物品ID +@property (nonatomic, copy, nonnull) NSString* goodsID; +//物品名称 +@property (nonatomic, copy, nonnull) NSString* goodsName; +//物品类型 +@property (nonatomic, copy, nonnull) NSString* goodsType; +//货币类型 默认CNY +@property (nonatomic, assign) JANALYTICSPurchaseCurrency currency; +//物品数量 +@property (nonatomic, assign) NSInteger quantity; + +@end + +/** + 内容浏览事件对象 + */ +@interface JANALYTICSBrowseEvent : JANALYTICSEventObject + +//内容名称,非空 +@property (nonatomic, copy, nonnull) NSString* name; +//内容ID +@property (nonatomic, copy, nonnull) NSString* contentID; +//内容类型 +@property (nonatomic, copy, nonnull) NSString* type; +//内容时长 +@property (nonatomic, assign) CGFloat duration; + +@end + +/** + 自定义计数事件对象 + */ +@interface JANALYTICSCountEvent : JANALYTICSEventObject + +//事件ID 非空 +@property (nonatomic, copy, nonnull) NSString* eventID; + +@end + +/** + 自定义计算事件对象 + */ +@interface JANALYTICSCalculateEvent : JANALYTICSEventObject + +//事件ID 非空 +@property (nonatomic, copy, nonnull) NSString* eventID; +//事件值 非空 +@property (nonatomic, assign) CGFloat value; + +@end + +typedef NS_ENUM(NSUInteger, JANALYTICSSex) { + //未知的 + JANALYTICSSexUnknown, + //男性 + JANALYTICSSexMale, + //女性 + JANALYTICSSexFemale, +}; + +typedef NS_ENUM(NSUInteger, JANALYTICSPaid) { + //未知 + JANALYTICSPaidUnknown, + //付费 + JANALYTICSPaidPaid, + //未付费 + JANALYTICSPaidUnpaid, +}; + +/** + 用户信息 + */ +@interface JANALYTICSUserInfo : NSObject + +/** + 账号ID、必填非空 + */ +@property (nonatomic, copy, nonnull) NSString * accountID; +/* + * 以下为极光内置用户维度 + * 当主动设置为nil时会删除该维度 + */ +//账号创建时间、时间戳 +@property (nonatomic, assign) NSTimeInterval creationTime; +//不能使用枚举意外的值 +@property (nonatomic, assign) JANALYTICSSex sex; +//出生年月,yyyyMMdd格式校验 +@property (nonatomic, copy, nullable) NSString * birthdate; +//不能使用枚举以外的值 +@property (nonatomic, assign) JANALYTICSPaid paid; +//手机号码 +@property (nonatomic, copy, nullable) NSString * phone; +//email +@property (nonatomic, copy, nullable) NSString * email; +//用户名 +@property (nonatomic, copy, nullable) NSString * name; +//微信id +@property (nonatomic, copy, nullable) NSString * wechatID; +//QQid +@property (nonatomic, copy, nullable) NSString * qqID; +//新浪微博id +@property (nonatomic, copy, nullable) NSString * weiboID; + +//用户自定义维度 key-value value只能为NSNumber/NSString/nil +//当value为nil时将会删除对应的维度 +- (void)setExtraObject:(nullable id)obj forKey:(nonnull NSString *)key; + +@end + diff --git a/msext/Class/libs/JANALYTICSService.h b/msext/Class/libs/JANALYTICSService.h new file mode 100755 index 0000000..8405ae0 --- /dev/null +++ b/msext/Class/libs/JANALYTICSService.h @@ -0,0 +1,121 @@ +/* + * | | | | \ \ / / | | | | / _______| + * | |____| | \ \/ / | |____| | / / + * | |____| | \ / | |____| | | | _____ + * | | | | / \ | | | | | | |____ | + * | | | | / /\ \ | | | | \ \______| | + * | | | | /_/ \_\ | | | | \_________| + * + * Copyright (c) 2016~ Shenzhen HXHG. All rights reserved. + */ + +#define JANALYTICS_VERSION_NUMBER 1.2.1 + +#import +#import "JANALYTICSEventObject.h" + +@interface JANALYTICSLaunchConfig : NSObject + +/* appKey 一个JPush 应用必须的,唯一的标识. 请参考 JPush 相关说明文档来获取这个标识. */ +@property (nonatomic, copy) NSString *appKey; +/* channel 发布渠道. 可选 */ +@property (nonatomic, copy) NSString *channel; +/* advertisingIdentifier 广告标识符(IDFA). 可选,IDFA能帮助您更准确的统计*/ +@property (nonatomic, copy) NSString *advertisingId; +/* isProduction 是否生产环境. 如果为开发状态,设置为NO; 如果为生产状态,应改为 YES.默认为NO */ +@property (nonatomic, assign) BOOL isProduction; + +@end + +@class CLLocation; + +@interface JANALYTICSService : NSObject + +/*! + * @abstract 启动SDK + * + * @param config SDK启动相关模型,必填 + */ ++ (void)setupWithConfig:(JANALYTICSLaunchConfig *)config; + +/*! + * @abstract 开始记录页面停留 + * + * @param pageName 页面名称 + */ ++ (void)startLogPageView:(NSString *)pageName; + +/*! + * @abstract 停止记录页面停留 + * + * @param pageName 页面 + * @discussion 停止后,默认即时上报此页面。可通过[setFrequency:]方法更改为周期性上报策略 + */ ++ (void)stopLogPageView:(NSString *)pageName; + +/*! + * @abstract 地理位置上报 + * + * @param latitude 纬度. + * @param longitude 经度. + * + */ ++ (void)setLatitude:(double)latitude longitude:(double)longitude; + +/*! + * @abstract 地理位置上报 + * + * @param location 直接传递 CLLocation * 型的地理信息 + * + * @discussion 需要链接 CoreLocation.framework 并且 #import + */ ++ (void)setLocation:(CLLocation *)location; + +/*!事件统计 + * @param event 上报的事件模型 + * @discussion 默认即时上报事件。可通过[setFrequency:]方法更改为周期性上报策略 + */ ++ (void)eventRecord:(JANALYTICSEventObject *)event; + +/** + 设置用户信息 + + @param userInfo 用户信息模型 + @param completion 错误码和错误信息callback + */ ++ (void)identifyAccount:(JANALYTICSUserInfo *)userInfo with:(void (^)(NSInteger err, NSString * msg))completion; + +/** + 解绑当前的用户信息 + */ ++ (void)detachAccount:(void (^)(NSInteger err, NSString * msg))completion; + +/** + 设置周期上报频率 + 默认为未设置频率,即时上报 + @param frequency 周期上报频率单位秒 + 频率区间:0 或者 10 < frequency < 24*60*60 + 可以设置为0,即表示取消周期上报频率,改为即时上报 + e.g. 十分钟上报一次 [JANALYTICSService setFrequency:600]; + */ ++ (void)setFrequency:(NSUInteger)frequency; + +/*! + * @abstract 开启Crash日志收集 + * + * @discussion 默认是关闭状态. + */ ++ (void)crashLogON; + +/*! + * @abstract 设置是否打印sdk产生的Debug级log信息, 默认为NO(不打印log) + * + * SDK 默认开启的日志级别为: Info. 只显示必要的信息, 不打印调试日志. + * + * 请在SDK启动后调用本接口,调用本接口可打开日志级别为: Debug, 打印调试日志. + * 请在发布产品时改为NO,避免产生不必要的IO + */ ++ (void)setDebug:(BOOL)enable; + +@end + diff --git a/msext/Class/libs/janalytics-ios-1.2.1.a b/msext/Class/libs/janalytics-ios-1.2.1.a new file mode 100755 index 0000000..3365680 Binary files /dev/null and b/msext/Class/libs/janalytics-ios-1.2.1.a differ diff --git a/msext/Class/libs/jcore-ios-1.1.8.a b/msext/Class/libs/jcore-ios-1.1.8.a new file mode 100755 index 0000000..4100867 Binary files /dev/null and b/msext/Class/libs/jcore-ios-1.1.8.a differ diff --git a/msext/Class/record/ChatRecorderView.h b/msext/Class/record/ChatRecorderView.h new file mode 100755 index 0000000..9bf6388 --- /dev/null +++ b/msext/Class/record/ChatRecorderView.h @@ -0,0 +1,26 @@ +// +// ChatRecorderView.h +// Jeans +// +// Created by Jeans on 3/24/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import + + + +@interface ChatRecorderView : UIView + +@property (retain, nonatomic) IBOutlet UIImageView *peakMeterIV; + +@property (retain, nonatomic) IBOutlet UIView *background; +@property (retain, nonatomic) IBOutlet UILabel *countDownLabel; + +//还原界面 +- (void)restoreDisplay; + +//更新音频峰值 +- (void)updateMetersByAvgPower:(float)_avgPower; + +@end diff --git a/msext/Class/record/ChatRecorderView.m b/msext/Class/record/ChatRecorderView.m new file mode 100755 index 0000000..b42a166 --- /dev/null +++ b/msext/Class/record/ChatRecorderView.m @@ -0,0 +1,114 @@ +// +// ChatRecorderView.m +// Jeans +// +// Created by Jeans on 3/24/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import "ChatRecorderView.h" + +@interface ChatRecorderView(){ + NSArray *peakImageAry; +} + +@end + +@implementation ChatRecorderView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + [self initilization]; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder{ + self = [super initWithCoder:aDecoder]; + if (self) { + [self initilization]; + } + return self; +} + +- (void)initilization{ + //初始化音量peak峰值图片数组 + + peakImageAry = [[NSArray alloc]initWithObjects: + [UIImage imageNamed:@"record_animate_01.png"], + [UIImage imageNamed:@"record_animate_02.png"], + [UIImage imageNamed:@"record_animate_03.png"], + [UIImage imageNamed:@"record_animate_04.png"], + [UIImage imageNamed:@"record_animate_05.png"], + [UIImage imageNamed:@"record_animate_06.png"], + [UIImage imageNamed:@"record_animate_07.png"], + [UIImage imageNamed:@"record_animate_08.png"], + [UIImage imageNamed:@"record_animate_09.png"], + [UIImage imageNamed:@"record_animate_10.png"], + [UIImage imageNamed:@"record_animate_11.png"], + [UIImage imageNamed:@"record_animate_12.png"], + [UIImage imageNamed:@"record_animate_13.png"], + [UIImage imageNamed:@"record_animate_14.png"], + nil]; + +} + +- (void)dealloc { + [peakImageAry release]; + [_peakMeterIV release]; + [_countDownLabel release]; + [super dealloc]; +} + +#pragma mark -还原显示界面 +- (void)restoreDisplay{ + //还原录音图 + self.background.layer.masksToBounds = YES; + self.background.layer.cornerRadius = 8; + + _peakMeterIV.image = [peakImageAry objectAtIndex:0]; + + //还原倒计时文本 + _countDownLabel.text = @""; +} + + + +#pragma mark - 更新音频峰值 +- (void)updateMetersByAvgPower:(float)_avgPower{ + //-160表示完全安静,0表示最大输入值 + // + NSInteger imageIndex = 0; + if (_avgPower >= -56 && _avgPower < -52) + imageIndex = 1; + else if (_avgPower >= -52 && _avgPower < -48) + imageIndex = 2; + else if (_avgPower >= -48 && _avgPower < -44) + imageIndex = 3; + else if (_avgPower >= -44 && _avgPower < -40) + imageIndex = 4; + else if (_avgPower >= -40 && _avgPower < -36) + imageIndex = 5; + else if (_avgPower >= -36 && _avgPower < -34) + imageIndex = 6; + else if (_avgPower >= -34 && _avgPower < -30) + imageIndex = 7; + else if (_avgPower >= -30 && _avgPower < -26) + imageIndex = 8; + else if (_avgPower >= -26 && _avgPower < -22) + imageIndex = 9; + else if (_avgPower >= -22 && _avgPower < -18) + imageIndex = 10; + else if (_avgPower >= -18 && _avgPower < -14) + imageIndex = 11; + else if (_avgPower >= -14 && _avgPower < -10) + imageIndex = 12; + else if (_avgPower >= -10) + imageIndex = 13; + + _peakMeterIV.image = [peakImageAry objectAtIndex:imageIndex]; +} + +@end diff --git a/msext/Class/record/ChatRecorderView.xib b/msext/Class/record/ChatRecorderView.xib new file mode 100755 index 0000000..124a526 --- /dev/null +++ b/msext/Class/record/ChatRecorderView.xib @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msext/Class/record/ChatVoiceRecorderVC.h b/msext/Class/record/ChatVoiceRecorderVC.h new file mode 100755 index 0000000..9eb0931 --- /dev/null +++ b/msext/Class/record/ChatVoiceRecorderVC.h @@ -0,0 +1,21 @@ +// +// ChatVoiceRecorderVC.h +// Jeans +// +// Created by Jeans on 3/23/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import "VoiceRecorderBaseVC.h" + + +#define kRecorderViewRect CGRectMake(([[UIScreen mainScreen]bounds].size.width-125)/2, 98, 125, 130) +//#define kCancelOriginY (kRecorderViewRect.origin.y + kRecorderViewRect.size.height + 180) +#define kCancelOriginY ([[UIScreen mainScreen]bounds].size.width-70) + +@interface ChatVoiceRecorderVC : VoiceRecorderBaseVC + +//开始录音 +- (void)beginRecordByFileName:(NSString*)_fileName; + +@end diff --git a/msext/Class/record/ChatVoiceRecorderVC.m b/msext/Class/record/ChatVoiceRecorderVC.m new file mode 100755 index 0000000..5744c3c --- /dev/null +++ b/msext/Class/record/ChatVoiceRecorderVC.m @@ -0,0 +1,288 @@ +// +// ChatVoiceRecorderVC.m +// Jeans +// +// Created by Jeans on 3/23/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import "ChatVoiceRecorderVC.h" +#import "UIView+Animation.h" +#import "ChatRecorderView.h" + +@interface ChatVoiceRecorderVC (){ + CGFloat curCount; //当前计数,初始为0 + ChatRecorderView *recorderView; //录音界面 + CGPoint curTouchPoint; //触摸点 + BOOL canNotSend; //不能发送 + NSTimer *timer; +} + +@property (retain, nonatomic) AVAudioRecorder *recorder; + +@end + +@implementation ChatVoiceRecorderVC +@synthesize recorder; + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view from its nib. +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)dealloc{ + [recorder release]; + [recorderView release]; + [super dealloc]; +} +- (BOOL)canRecord +{ + __block BOOL bCanRecord = YES; + if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0"] != NSOrderedAscending) + { + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + if ([audioSession respondsToSelector:@selector(requestRecordPermission:)]) { + [audioSession performSelector:@selector(requestRecordPermission:) withObject:^(BOOL granted) { + if (granted) { + bCanRecord = YES; + } else { + bCanRecord = NO; + } + }]; + } + } + + return bCanRecord; + + +} + +//-(int)ifauth +//{ +// int flag; +// AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; +// switch (authStatus) { +// case AVAuthorizationStatusNotDetermined: +// //没有询问是否开启麦克风 +// flag = 1; +// break; +// case AVAuthorizationStatusRestricted: +// //未授权,家长限制 +// flag = 0; +// break; +// case AVAuthorizationStatusDenied: +// //玩家未授权 +// flag = 0; +// break; +// case AVAuthorizationStatusAuthorized: +// //玩家授权 +// flag = 2; +// break; +// default: +// break; +// } +// return flag; +//} + +#pragma mark - 开始录音 +- (void)beginRecordByFileName:(NSString*)_fileName;{ + + + //设置文件名和录音路径 + self.recordFileName = _fileName; + self.recordFilePath = [VoiceRecorderBaseVC getPathByFileName:recordFileName ofType:@"wav"]; + + //初始化录音 + self.recorder = [[[AVAudioRecorder alloc]initWithURL:[NSURL URLWithString:recordFilePath] + settings:[VoiceRecorderBaseVC getAudioRecorderSettingDict] + error:nil]autorelease]; + recorder.delegate = self; + recorder.meteringEnabled = YES; + + [recorder prepareToRecord]; + + //还原计数 + curCount = 0; + //还原发送 + canNotSend = NO; + + //开始录音 + [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayAndRecord error:nil]; + [[AVAudioSession sharedInstance] setActive:YES error:nil]; + + + [recorder record]; + + //启动计时器 + [self startTimer]; + + //显示录音界面 + [self initRecordView]; + [UIView showView:recorderView + animateType:AnimateTypeOfPopping + finalRect:kRecorderViewRect + completion:^(BOOL finish){ + if (finish){ + //注册nScreenTouch事件 + [self addScreenTouchObserver]; + } + }]; + //设置遮罩背景不可触摸 + [UIView setTopMaskViewCanTouch:NO]; +} +#pragma mark - 初始化录音界面 +- (void)initRecordView{ + if (recorderView == nil) + recorderView = (ChatRecorderView*)[[[[NSBundle mainBundle]loadNibNamed:@"ChatRecorderView" owner:self options:nil]lastObject]retain]; + //还原界面显示 + [recorderView restoreDisplay]; +} +#pragma mark - 启动定时器 +- (void)startTimer{ + timer = [NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(updateMeters) userInfo:nil repeats:YES]; +} + +#pragma mark - 停止定时器 +- (void)stopTimer{ + if (timer && timer.isValid){ + [timer invalidate]; + timer = nil; + } +} +#pragma mark - 更新音频峰值 +- (void)updateMeters{ + if (recorder.isRecording){ + + //更新峰值 + [recorder updateMeters]; + [recorderView updateMetersByAvgPower:[recorder averagePowerForChannel:0]]; +// NSLog(@"峰值:%f",[recorder averagePowerForChannel:0]); + + //倒计时 + if (curCount >= maxRecordTime - 10 && curCount < maxRecordTime) { + //剩下10秒 + recorderView.countDownLabel.text = [NSString stringWithFormat:@"录音剩下:%d秒",(int)(maxRecordTime-curCount)]; + }else if (curCount >= maxRecordTime){ + //时间到 + [self touchEnded:curTouchPoint]; + } + curCount += 0.1f; + } +} + +#pragma mark - 移除触摸观察者 +- (void)removeScreenTouchObserver{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"nScreenTouch" object:nil];//移除nScreenTouch事件 +} +#pragma mark - 添加触摸观察者 +- (void)addScreenTouchObserver{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onScreenTouch:) name:@"nScreenTouch" object:nil]; +} +-(void)onScreenTouch:(NSNotification *)notification { + UIEvent *event=[notification.userInfo objectForKey:@"data"]; + NSSet *allTouches = event.allTouches; + + //如果未触摸或只有单点触摸 + if ((curTouchPoint.x == CGPointZero.x && curTouchPoint.y == CGPointZero.y) || allTouches.count == 1) + [self transferTouch:[allTouches anyObject]]; + else{ + //遍历touch,找到最先触摸的那个touch + for (UITouch *touch in allTouches){ + CGPoint prePoint = [touch previousLocationInView:nil]; + + if (prePoint.x == curTouchPoint.x && prePoint.y == curTouchPoint.y) + [self transferTouch:touch]; + } + } +} +//传递触点 +- (void)transferTouch:(UITouch*)_touch{ + CGPoint point = [_touch locationInView:nil]; + switch (_touch.phase) { + case UITouchPhaseBegan: + [self touchBegan:point]; + break; + case UITouchPhaseMoved: + [self touchMoved:point]; + break; + case UITouchPhaseCancelled: + case UITouchPhaseEnded: + [self touchEnded:point]; + break; + default: + break; + } +} +#pragma mark - 触摸开始 +- (void)touchBegan:(CGPoint)_point{ + curTouchPoint = _point; +} +#pragma mark - 触摸移动 +- (void)touchMoved:(CGPoint)_point{ + curTouchPoint = _point; + //判断是否移动到取消区域 + // canNotSend = _point.y < kCancelOriginY ? YES : NO; + + canNotSend=NO; +} +#pragma mark - 触摸结束 +- (void)touchEnded:(CGPoint)_point{ + //停止计时器 + [self stopTimer]; + + curTouchPoint = CGPointZero; + [self removeScreenTouchObserver]; + + [UIView hideViewByCompletion:^(BOOL finish){ + + //停止录音 + if (recorder.isRecording) + [recorder stop]; + + if (canNotSend) { + //取消发送,删除文件 + [VoiceRecorderBaseVC deleteFileAtPath:recordFilePath]; + }else{ + //回调录音文件路径 + if ([self.vrbDelegate respondsToSelector:@selector(VoiceRecorderBaseVCRecordFinish:fileName:)]) + [self.vrbDelegate VoiceRecorderBaseVCRecordFinish:recordFilePath fileName:recordFileName]; + } + }]; +} + + +#pragma mark - AVAudioRecorder Delegate Methods +- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{ + NSLog(@"录音停止"); + + [self stopTimer]; + curCount = 0; +} +- (void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder{ + NSLog(@"录音开始"); + [self stopTimer]; + curCount = 0; +} +- (void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder withOptions:(NSUInteger)flags{ + NSLog(@"录音中断"); + [self stopTimer]; + curCount = 0; +} + +@end diff --git a/msext/Class/record/ChatVoiceRecorderVC.xib b/msext/Class/record/ChatVoiceRecorderVC.xib new file mode 100755 index 0000000..c3f4f32 --- /dev/null +++ b/msext/Class/record/ChatVoiceRecorderVC.xib @@ -0,0 +1,127 @@ + + + + 1296 + 14C1514 + 6751 + 1344.72 + 757.30 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 6736 + + + IBProxyObject + IBUIView + + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + PluginDependencyRecalculationVersion + + + + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + {{0, 20}, {320, 460}} + + 3 + MQA + + 2 + + + + + IBUIScreenMetrics + IBCocoaTouchFramework + iPhone 3.5-inch + + YES + + + + + + {320, 480} + {480, 320} + + + 0 + + IBCocoaTouchFramework + + + + + + + view + + + + 3 + + + + + + 0 + + + + + + 1 + + + + + + -1 + + + File's Owner + + + -2 + + + + + + + ChatVoiceRecorderVC + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + UIResponder + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + + + 4 + + + 0 + IBCocoaTouchFramework + NO + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + 3 + + diff --git a/msext/Class/record/CustomWindow.h b/msext/Class/record/CustomWindow.h new file mode 100755 index 0000000..5937f4d --- /dev/null +++ b/msext/Class/record/CustomWindow.h @@ -0,0 +1,13 @@ +// +// CustomWindow.h +// AmrConvertAndRecord +// +// Created by Jeans on 3/29/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import + +@interface CustomWindow : UIWindow + +@end diff --git a/msext/Class/record/CustomWindow.m b/msext/Class/record/CustomWindow.m new file mode 100755 index 0000000..78b6770 --- /dev/null +++ b/msext/Class/record/CustomWindow.m @@ -0,0 +1,37 @@ +// +// CustomWindow.m +// AmrConvertAndRecord +// +// Created by Jeans on 3/29/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import "CustomWindow.h" + +@implementation CustomWindow + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code + } + return self; +} + +-(void)sendEvent:(UIEvent *)event { + @try { + + if (event.type == UIEventTypeTouches) {//发送一个名为‘nScreenTouch’(自定义)的事件 + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:@"nScreenTouch" object:nil userInfo:[NSDictionary dictionaryWithObject:event forKey:@"data"]]]; + } + [super sendEvent:event]; + } @catch (NSException *exception) { + NSLog(@"%@",exception); + } @finally { + NSLog(@"@finally"); + } +} + + +@end diff --git a/msext/Class/record/UIViewAnimation/UIView+Animation.h b/msext/Class/record/UIViewAnimation/UIView+Animation.h new file mode 100755 index 0000000..6f3a23a --- /dev/null +++ b/msext/Class/record/UIViewAnimation/UIView+Animation.h @@ -0,0 +1,56 @@ +// +// UIView+Animation.h +// +// +// Created by Jeans on 3/9/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import + +#define kDefaultAnimateTime 0.25f + +typedef enum AnimateType{ //动画类型 + AnimateTypeOfTV, //电视 + AnimateTypeOfPopping, //弹性缩小放大 + AnimateTypeOfLeft, //左 + AnimateTypeOfRight, //右 + AnimateTypeOfTop, //上 + AnimateTypeOfBottom //下 +}AnimateType; + +@interface UIView (Animation) + +#pragma mark - 获取顶部View ++ (UIView *)getTopView; + +#pragma mark - 顶层maskView触摸 ++ (void)setTopMaskViewCanTouch:(BOOL)_canTouch; + +/** + 显示view + @param _view 需要显示的view + @param _aType 动画类型 + @param _fRect 最终位置 + */ ++ (void)showView:(UIView*)_view animateType:(AnimateType)_aType finalRect:(CGRect)_fRect; + + +/** + 消失view + */ ++ (void)hideView; + + +/** + 消失view + @param _aType 动画类型 + */ ++ (void)hideViewByType:(AnimateType)_aType; + + +#pragma mark - 下面的增加了完成块 ++ (void)showView:(UIView*)_view animateType:(AnimateType)_aType finalRect:(CGRect)_fRect completion:(void(^)(BOOL finished))completion; ++ (void)hideViewByCompletion:(void(^)(BOOL finished))completion; ++ (void)hideViewByType:(AnimateType)_aType completion:(void(^)(BOOL finished))completion; +@end diff --git a/msext/Class/record/UIViewAnimation/UIView+Animation.m b/msext/Class/record/UIViewAnimation/UIView+Animation.m new file mode 100755 index 0000000..822bc81 --- /dev/null +++ b/msext/Class/record/UIViewAnimation/UIView+Animation.m @@ -0,0 +1,286 @@ +// +// UIView+Animation.m +// +// +// Created by Jeans on 3/9/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import "UIView+Animation.h" +#import "AppDelegate.h" + +#define kScaleMin 0.007f +#define kScaleDefault 1.0f +#define kScaleDelta 0.05f + + +#define kFirstAnimateTime 0.3f +#define kSecondAnimateTime 0.2f + +#define kMaskViewFinalAlpha 0.2f //背景的透明度 + +@interface ViewInfo : NSObject{ + AnimateType aType; //动画类型 + UIView *displayView; //显示页面 + CGRect displayRect; //显示的位置 + UIControl *maskView; //遮挡页面 + void (^showBlock)(BOOL finished); + void (^hideBlock)(BOOL finished); +} + +@property (retain, nonatomic) UIView *displayView; +@property (assign, nonatomic) AnimateType aType; +@property (assign, nonatomic) CGRect displayRect; +@property (retain, nonatomic) UIControl *maskView; +@property (copy, nonatomic) void (^showBlock)(BOOL finished); +@property (copy, nonatomic) void (^hideBlock)(BOOL finished); +@end + +@implementation ViewInfo + +@synthesize displayView,aType,maskView,displayRect,showBlock,hideBlock; +- (void)dealloc{ + Block_release(hideBlock); + Block_release(showBlock); + [maskView release]; + [displayView release]; + [super dealloc]; +} + +@end + +@implementation UIView (Animation) + +static NSMutableArray *displayViewAry;//已显示的页面数组 + +#pragma mark - 获取顶部View ++ (AppDelegate *)getAppDelegate { + return (AppDelegate *)[[UIApplication sharedApplication] delegate]; +} ++ (UIView *)getTopView{ + return [[UIApplication sharedApplication].windows objectAtIndex:0]; + // return [FuncPublic SharedFuncPublic].currentVC.view; + return [[[UIView getAppDelegate] root] view]; +} + +#pragma mark - 顶层maskView触摸 ++ (void)setTopMaskViewCanTouch:(BOOL)_canTouch{ + ViewInfo *info = [displayViewAry lastObject]; + if (_canTouch) + [info.maskView addTarget:self action:@selector(maskViewTouch) forControlEvents:UIControlEventTouchUpInside]; + else + [info.maskView removeTarget:self action:@selector(maskViewTouch) forControlEvents:UIControlEventTouchUpInside]; +} + + +#pragma mark - 下面的增加了完成块 +/** + 显示view + @param _view 需要显示的view + @param _aType 动画类型 + @param _fRect 最终位置 + @param completion 动画块 + */ ++ (void)showView:(UIView*)_view animateType:(AnimateType)_aType finalRect:(CGRect)_fRect completion:(void(^)(BOOL finished))completion{ + //初始化页面数组 + if (displayViewAry == nil) + displayViewAry = [[NSMutableArray alloc]init]; + + UIView *topView = [UIView getTopView]; + + //存储页面信息 + ViewInfo *info = [[ViewInfo alloc]init]; + info.displayView = _view; + info.aType = _aType; + info.displayRect = _fRect; + + //初始化遮罩页面 + UIControl *maskView = [[UIControl alloc]init]; + maskView.backgroundColor = [UIColor blackColor]; + maskView.alpha = 0; + maskView.frame = topView.bounds; + [maskView addTarget:self action:@selector(maskViewTouch) forControlEvents:UIControlEventTouchUpInside]; + //添加页面 + [topView addSubview:maskView]; + [topView bringSubviewToFront:maskView]; + + info.maskView = maskView; + [maskView release]; + + if (completion) + info.showBlock = completion; + + [displayViewAry addObject:info]; + [info release]; + + + //根据不同的动画类型显示 + switch (_aType) { + case AnimateTypeOfTV: + [UIView showTV]; + break; + case AnimateTypeOfPopping: + [UIView showPopping]; + default: + break; + } +} +/** + 显示view + @param _view 需要显示的view + @param _aType 动画类型 + @param _fRect 最终位置 + */ ++ (void)showView:(UIView*)_view animateType:(AnimateType)_aType finalRect:(CGRect)_fRect{ + [self showView:_view animateType:_aType finalRect:_fRect completion:nil]; +} + + +#pragma mark - 消失view + ++ (void)hideViewByCompletion:(void(^)(BOOL finished))completion{ + if ([displayViewAry count] > 0){ + ViewInfo *info = [displayViewAry lastObject]; + if (completion) + info.hideBlock = completion; + } + [UIView maskViewTouch]; +} ++ (void)hideViewByType:(AnimateType)_aType completion:(void(^)(BOOL finished))completion{ + if ([displayViewAry count] > 0){ + ViewInfo *info = [displayViewAry lastObject]; + info.aType = _aType; + if (completion) + info.hideBlock = completion; + } + [UIView maskViewTouch]; +} +// ++ (void)hideView{ + [UIView hideViewByCompletion:nil]; +} ++ (void)hideViewByType:(AnimateType)_aType{ + [UIView hideViewByType:_aType completion:nil]; +} + +#pragma mark - 触摸背景 ++ (void)maskViewTouch{ + if ([displayViewAry count] > 0){ + ViewInfo *info = [displayViewAry lastObject]; + + //根据不同类型隐藏 + switch (info.aType) { + case AnimateTypeOfTV: + [UIView hideTV]; + break; + case AnimateTypeOfPopping: + [UIView hidePopping]; + break; + default: + break; + } + } +} +#pragma mark - 移除遮罩和已显示页面 ++ (void)removeMaskViewAndDisplay:(ViewInfo*)info{ + if (info.aType == AnimateTypeOfTV || info.aType == AnimateTypeOfPopping) //TV,Popping 类型需要还原 + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault, kScaleDefault); + + [info.displayView removeFromSuperview]; + [info.maskView removeFromSuperview]; + [displayViewAry removeObject:info]; +} + +#pragma mark - 显示动画 + +#pragma mark - TV 显示 ++ (void)showTV{ + ViewInfo *info = [displayViewAry lastObject]; + UIView *topView = [UIView getTopView]; + info.displayView.frame = info.displayRect; + [topView addSubview:info.displayView]; + [topView bringSubviewToFront:info.displayView]; + + info.displayView.transform = CGAffineTransformMakeScale(kScaleMin, kScaleMin); + + //开始动画 + [UIView animateWithDuration:kSecondAnimateTime animations:^{ + info.maskView.alpha = 0.1f; + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault, kScaleMin); + }completion:^(BOOL finish){ + [UIView animateWithDuration:kFirstAnimateTime animations:^{ + info.maskView.alpha = kMaskViewFinalAlpha; + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault, kScaleDefault); + }completion:^(BOOL finish){ + //调用完成动画块 + if (info.showBlock) + info.showBlock(finish); + }]; + }]; +} +#pragma mark - TV 消失 ++ (void)hideTV{ + + ViewInfo *info = [displayViewAry lastObject]; + + [UIView animateWithDuration:kSecondAnimateTime animations:^{ + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault, kScaleMin); + }completion:^(BOOL finish){ + [UIView animateWithDuration:kFirstAnimateTime animations:^{ + info.displayView.transform = CGAffineTransformMakeScale(kScaleMin, kScaleMin); + info.maskView.alpha = 0; + }completion:^(BOOL finish){ + //调用完成动画块 + if (info.hideBlock) + info.hideBlock(finish); + [UIView removeMaskViewAndDisplay:info]; + }]; + }]; +} + +#pragma mark - Popping 显示 ++ (void)showPopping{ + ViewInfo *info = [displayViewAry lastObject]; + UIView *topView = [UIView getTopView]; + info.displayView.frame = info.displayRect; + [topView addSubview:info.displayView]; + [topView bringSubviewToFront:info.displayView]; + + info.displayView.transform = CGAffineTransformMakeScale(kScaleMin, kScaleMin); + + //开始动画 + [UIView animateWithDuration:kFirstAnimateTime animations:^{ + info.maskView.alpha = kMaskViewFinalAlpha; + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault+kScaleDelta, kScaleDefault+kScaleDelta); + }completion:^(BOOL finish){ + [UIView animateWithDuration:kSecondAnimateTime animations:^{ + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault-kScaleDelta, kScaleDefault-kScaleDelta); + }completion:^(BOOL finish){ + [UIView animateWithDuration:kSecondAnimateTime animations:^{ + info.displayView.transform = CGAffineTransformMakeScale(kScaleDefault, kScaleDefault); + }completion:^(BOOL finish){ + //调用完成动画块 + if (info.showBlock) + info.showBlock(finish); + }]; + }]; + }]; +} +#pragma mark - Popping 消失 ++ (void)hidePopping{ + ViewInfo *info = [displayViewAry lastObject]; + + [UIView animateWithDuration:kFirstAnimateTime animations:^{ + info.maskView.alpha = 0; + info.displayView.transform = CGAffineTransformMakeScale(kScaleMin, kScaleMin); + }completion:^(BOOL finish){ + //调用完成动画块 + if (info.hideBlock) + info.hideBlock(finish); + [UIView removeMaskViewAndDisplay:info]; + }]; +} + + + +@end diff --git a/msext/Class/record/VoiceRecorderBaseVC.h b/msext/Class/record/VoiceRecorderBaseVC.h new file mode 100755 index 0000000..2bfc9f2 --- /dev/null +++ b/msext/Class/record/VoiceRecorderBaseVC.h @@ -0,0 +1,81 @@ +// +// VoiceRecorderBaseVC.h +// Jeans +// +// Created by Jeans on 3/23/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import +#import "AudioToolbox/AudioToolbox.h" +#import +#import + +//默认最大录音时间 +#define kDefaultMaxRecordTime 60 + +@protocol VoiceRecorderBaseVCDelegate + +//录音完成回调,返回文件路径和文件名 +- (void)VoiceRecorderBaseVCRecordFinish:(NSString *)_filePath fileName:(NSString*)_fileName; + +@end + +@interface VoiceRecorderBaseVC : UIViewController{ + +@protected + NSInteger maxRecordTime; //最大录音时间 + NSString *recordFileName;//录音文件名 + NSString *recordFilePath;//录音文件路径 +} + +@property (assign, nonatomic) id vrbDelegate; + +@property (assign, nonatomic) NSInteger maxRecordTime;//最大录音时间 +@property (copy, nonatomic) NSString *recordFileName;//录音文件名 +@property (copy, nonatomic) NSString *recordFilePath;//录音文件路径 + +/** + 生成当前时间字符串 + @returns 当前时间字符串 + */ ++ (NSString*)getCurrentTimeString; + +/** + 获取缓存路径 + @returns 缓存路径 + */ ++ (NSString*)getCacheDirectory; + +/** + 判断文件是否存在 + @param _path 文件路径 + @returns 存在返回yes + */ ++ (BOOL)fileExistsAtPath:(NSString*)_path; + +/** + 删除文件 + @param _path 文件路径 + @returns 成功返回yes + */ ++ (BOOL)deleteFileAtPath:(NSString*)_path; + + +#pragma mark - + +/** + 生成文件路径 + @param _fileName 文件名 + @param _type 文件类型 + @returns 文件路径 + */ ++ (NSString*)getPathByFileName:(NSString *)_fileName; ++ (NSString*)getPathByFileName:(NSString *)_fileName ofType:(NSString *)_type; + +/** + 获取录音设置 + @returns 录音设置 + */ ++ (NSDictionary*)getAudioRecorderSettingDict; +@end diff --git a/msext/Class/record/VoiceRecorderBaseVC.m b/msext/Class/record/VoiceRecorderBaseVC.m new file mode 100755 index 0000000..2a5fab0 --- /dev/null +++ b/msext/Class/record/VoiceRecorderBaseVC.m @@ -0,0 +1,132 @@ +// +// VoiceRecorderBaseVC.m +// Jeans +// +// Created by Jeans on 3/23/13. +// Copyright (c) 2013 Jeans. All rights reserved. +// + +#import "VoiceRecorderBaseVC.h" + +@interface VoiceRecorderBaseVC () +@end + +@implementation VoiceRecorderBaseVC +@synthesize vrbDelegate,maxRecordTime,recordFileName,recordFilePath; + + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + maxRecordTime = kDefaultMaxRecordTime; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view. +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)dealloc{ + [recordFilePath release]; + [recordFileName release]; + [super dealloc]; +} + +/** + 生成当前时间字符串 + @returns 当前时间字符串 + */ ++ (NSString*)getCurrentTimeString +{ + NSDateFormatter *dateformat=[[[NSDateFormatter alloc]init]autorelease]; + [dateformat setDateFormat:@"yyyyMMddHHmmss"]; + return [dateformat stringFromDate:[NSDate date]]; +} + + +/** + 获取缓存路径 + @returns 缓存路径 + */ ++ (NSString*)getCacheDirectory +{ + return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]; + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + return [[paths objectAtIndex:0]stringByAppendingPathComponent:@"Voice"]; +} + +/** + 判断文件是否存在 + @param _path 文件路径 + @returns 存在返回yes + */ ++ (BOOL)fileExistsAtPath:(NSString*)_path +{ + return [[NSFileManager defaultManager] fileExistsAtPath:_path]; +} + +/** + 删除文件 + @param _path 文件路径 + @returns 成功返回yes + */ ++ (BOOL)deleteFileAtPath:(NSString*)_path +{ + return [[NSFileManager defaultManager] removeItemAtPath:_path error:nil]; +} + +/** + 生成文件路径 + @param _fileName 文件名 + @param _type 文件类型 + @returns 文件路径 + */ ++ (NSString*)getPathByFileName:(NSString *)_fileName ofType:(NSString *)_type +{ + NSString* fileDirectory = [[[VoiceRecorderBaseVC getCacheDirectory]stringByAppendingPathComponent:_fileName]stringByAppendingPathExtension:_type]; + return fileDirectory; +} + +/** + 生成文件路径 + @param _fileName 文件名 + @returns 文件路径 + */ ++ (NSString*)getPathByFileName:(NSString *)_fileName{ + NSString* fileDirectory = [[VoiceRecorderBaseVC getCacheDirectory]stringByAppendingPathComponent:_fileName]; + return fileDirectory; +} + +/** + 获取录音设置 + @returns 录音设置 + */ ++ (NSDictionary*)getAudioRecorderSettingDict +{ + NSDictionary *recordSetting = [[NSDictionary alloc] initWithObjectsAndKeys: + [NSNumber numberWithFloat: 8000.0],AVSampleRateKey, //采样率 + [NSNumber numberWithInt: kAudioFormatLinearPCM],AVFormatIDKey, + [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,//采样位数 默认 16 + [NSNumber numberWithInt: 1], AVNumberOfChannelsKey,//通道的数目 +// [NSNumber numberWithBool:NO],AVLinearPCMIsBigEndianKey,//大端还是小端 是内存的组织方式 +// [NSNumber numberWithBool:NO],AVLinearPCMIsFloatKey,//采样信号是整数还是浮点数 +// [NSNumber numberWithInt: AVAudioQualityMedium],AVEncoderAudioQualityKey,//音频编码质量 + nil]; + return [recordSetting autorelease]; +} + + + +@end diff --git a/msext/Class/record/img/record_animate_01.png b/msext/Class/record/img/record_animate_01.png new file mode 100755 index 0000000..b2dc09e Binary files /dev/null and b/msext/Class/record/img/record_animate_01.png differ diff --git a/msext/Class/record/img/record_animate_02.png b/msext/Class/record/img/record_animate_02.png new file mode 100755 index 0000000..7243c86 Binary files /dev/null and b/msext/Class/record/img/record_animate_02.png differ diff --git a/msext/Class/record/img/record_animate_03.png b/msext/Class/record/img/record_animate_03.png new file mode 100755 index 0000000..37a07ac Binary files /dev/null and b/msext/Class/record/img/record_animate_03.png differ diff --git a/msext/Class/record/img/record_animate_04.png b/msext/Class/record/img/record_animate_04.png new file mode 100755 index 0000000..b7e9b66 Binary files /dev/null and b/msext/Class/record/img/record_animate_04.png differ diff --git a/msext/Class/record/img/record_animate_05.png b/msext/Class/record/img/record_animate_05.png new file mode 100755 index 0000000..289641a Binary files /dev/null and b/msext/Class/record/img/record_animate_05.png differ diff --git a/msext/Class/record/img/record_animate_06.png b/msext/Class/record/img/record_animate_06.png new file mode 100755 index 0000000..3494ec5 Binary files /dev/null and b/msext/Class/record/img/record_animate_06.png differ diff --git a/msext/Class/record/img/record_animate_07.png b/msext/Class/record/img/record_animate_07.png new file mode 100755 index 0000000..33d4a41 Binary files /dev/null and b/msext/Class/record/img/record_animate_07.png differ diff --git a/msext/Class/record/img/record_animate_08.png b/msext/Class/record/img/record_animate_08.png new file mode 100755 index 0000000..25ca6b6 Binary files /dev/null and b/msext/Class/record/img/record_animate_08.png differ diff --git a/msext/Class/record/img/record_animate_09.png b/msext/Class/record/img/record_animate_09.png new file mode 100755 index 0000000..747f860 Binary files /dev/null and b/msext/Class/record/img/record_animate_09.png differ diff --git a/msext/Class/record/img/record_animate_10.png b/msext/Class/record/img/record_animate_10.png new file mode 100755 index 0000000..af293cc Binary files /dev/null and b/msext/Class/record/img/record_animate_10.png differ diff --git a/msext/Class/record/img/record_animate_11.png b/msext/Class/record/img/record_animate_11.png new file mode 100755 index 0000000..99da4f0 Binary files /dev/null and b/msext/Class/record/img/record_animate_11.png differ diff --git a/msext/Class/record/img/record_animate_12.png b/msext/Class/record/img/record_animate_12.png new file mode 100755 index 0000000..bfaa62c Binary files /dev/null and b/msext/Class/record/img/record_animate_12.png differ diff --git a/msext/Class/record/img/record_animate_13.png b/msext/Class/record/img/record_animate_13.png new file mode 100755 index 0000000..4cd47c0 Binary files /dev/null and b/msext/Class/record/img/record_animate_13.png differ diff --git a/msext/Class/record/img/record_animate_14.png b/msext/Class/record/img/record_animate_14.png new file mode 100755 index 0000000..6e14fe6 Binary files /dev/null and b/msext/Class/record/img/record_animate_14.png differ diff --git a/msext/Class/webCache/NSString+Sha1.h b/msext/Class/webCache/NSString+Sha1.h new file mode 100755 index 0000000..1e87fee --- /dev/null +++ b/msext/Class/webCache/NSString+Sha1.h @@ -0,0 +1,19 @@ + +#import +#import + +/** + * This extension contains several a helper + * for creating a sha1 hash from instances of NSString + */ +@interface NSString (Sha1) + +/** + * Creates a SHA1 (hash) representation of NSString. + * + * @return NSString + */ +- (NSString *)sha1; + + +@end diff --git a/msext/Class/webCache/NSString+Sha1.m b/msext/Class/webCache/NSString+Sha1.m new file mode 100755 index 0000000..9cbb950 --- /dev/null +++ b/msext/Class/webCache/NSString+Sha1.m @@ -0,0 +1,24 @@ + +#import "NSString+Sha1.h" + +@implementation NSString (Sha1) + +- (NSString *)sha1 +{ + // see http://www.makebetterthings.com/iphone/how-to-get-md5-and-sha1-in-objective-c-ios-sdk/ + NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t digest[CC_SHA1_DIGEST_LENGTH]; + + CC_SHA1(data.bytes, (CC_LONG)data.length, digest); + + NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; + + for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { + [output appendFormat:@"%02x", digest[i]]; + } + + return output; +} + + +@end diff --git a/msext/Class/webCache/README.md b/msext/Class/webCache/README.md new file mode 100755 index 0000000..3e891d4 --- /dev/null +++ b/msext/Class/webCache/README.md @@ -0,0 +1,60 @@ +# BACKGROUND + +RNCachingURLProtocol is a simple shim for the HTTP protocol (that’s not +nearly as scary as it sounds). Anytime a URL is downloaded, the response is +cached to disk. Anytime a URL is requested, if we’re online then things +proceed normally. If we’re offline, then we retrieve the cached version. + +The point of RNCachingURLProtocol is mostly to demonstrate how this is done. +The current implementation is extremely simple. In particular, it doesn’t +worry about cleaning up the cache. The assumption is that you’re caching just +a few simple things, like your “Latest News” page (which was the problem I +was solving). It caches all HTTP traffic, so without some modifications, it’s +not appropriate for an app that has a lot of HTTP connections (see +MKNetworkKit for that). But if you need to cache some URLs and not others, +that is easy to implement. + +You should also look at [AFCache](https://github.com/artifacts/AFCache) for a +more powerful caching engine that is currently integrating the ideas of +RNCachingURLProtocol. + +# USAGE + +1. To build, you will need the Reachability code from Apple (included). That requires that you link with + `SystemConfiguration.framework`. + +2. At some point early in the program (usually `application:didFinishLaunchingWithOptions:`), + call the following: + + `[NSURLProtocol registerClass:[RNCachingURLProtocol class]];` + +3. There is no step 3. + +For more details see + [Drop-in offline caching for UIWebView (and NSURLProtocol)](http://robnapier.net/blog/offline-uiwebview-nsurlprotocol-588). + +# EXAMPLE + +See the CachedWebView project for example usage. + +# LICENSE + + This code is licensed under the MIT License: + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/msext/Class/webCache/RNCachingURLProtocol.h b/msext/Class/webCache/RNCachingURLProtocol.h new file mode 100755 index 0000000..5c8bd13 --- /dev/null +++ b/msext/Class/webCache/RNCachingURLProtocol.h @@ -0,0 +1,71 @@ +// +// RNCachingURLProtocol.h +// +// Created by Robert Napier on 1/10/12. +// Copyright (c) 2012 Rob Napier. All rights reserved. +// +// This code is licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// RNCachingURLProtocol is a simple shim for the HTTP protocol (that’s not +// nearly as scary as it sounds). Anytime a URL is download, the response is +// cached to disk. Anytime a URL is requested, if we’re online then things +// proceed normally. If we’re offline, then we retrieve the cached version. +// +// The point of RNCachingURLProtocol is mostly to demonstrate how this is done. +// The current implementation is extremely simple. In particular, it doesn’t +// worry about cleaning up the cache. The assumption is that you’re caching just +// a few simple things, like your “Latest News” page (which was the problem I +// was solving). It caches all HTTP traffic, so without some modifications, it’s +// not appropriate for an app that has a lot of HTTP connections (see +// MKNetworkKit for that). But if you need to cache some URLs and not others, +// that is easy to implement. +// +// You should also look at [AFCache](https://github.com/artifacts/AFCache) for a +// more powerful caching engine that is currently integrating the ideas of +// RNCachingURLProtocol. +// +// A quick rundown of how to use it: +// +// 1. To build, you will need the Reachability code from Apple (included). That requires that you link with +// `SystemConfiguration.framework`. +// +// 2. At some point early in the program (application:didFinishLaunchingWithOptions:), +// call the following: +// +// `[NSURLProtocol registerClass:[RNCachingURLProtocol class]];` +// +// 3. There is no step 3. +// +// For more details see +// [Drop-in offline caching for UIWebView (and NSURLProtocol)](http://robnapier.net/blog/offline-uiwebview-nsurlprotocol-588). + +#import + +@interface RNCachingURLProtocol : NSURLProtocol + ++ (NSSet *)supportedSchemes; ++ (void)setSupportedSchemes:(NSSet *)supportedSchemes; + +- (NSString *)cachePathForRequest:(NSURLRequest *)aRequest; +- (BOOL) useCache; + +@end diff --git a/msext/Class/webCache/RNCachingURLProtocol.m b/msext/Class/webCache/RNCachingURLProtocol.m new file mode 100755 index 0000000..c247cf1 --- /dev/null +++ b/msext/Class/webCache/RNCachingURLProtocol.m @@ -0,0 +1,296 @@ +// +// RNCachingURLProtocol.m +// +// Created by Robert Napier on 1/10/12. +// Copyright (c) 2012 Rob Napier. +// +// This code is licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +#import "RNCachingURLProtocol.h" +#import "Reachability.h" +#import "NSString+Sha1.h" + +#define WORKAROUND_MUTABLE_COPY_LEAK 1 + +#if WORKAROUND_MUTABLE_COPY_LEAK +// required to workaround http://openradar.appspot.com/11596316 +@interface NSURLRequest(MutableCopyWorkaround) + +- (id) mutableCopyWorkaround; + +@end +#endif + +@interface RNCachedData : NSObject +@property (nonatomic, readwrite, strong) NSData *data; +@property (nonatomic, readwrite, strong) NSURLResponse *response; +@property (nonatomic, readwrite, strong) NSURLRequest *redirectRequest; +@end + +static NSString *RNCachingURLHeader = @"X-RNCache"; + +@interface RNCachingURLProtocol () // iOS5-only +@property (nonatomic, readwrite, strong) NSURLConnection *connection; +@property (nonatomic, readwrite, strong) NSMutableData *data; +@property (nonatomic, readwrite, strong) NSURLResponse *response; +- (void)appendData:(NSData *)newData; +@end + +static NSObject *RNCachingSupportedSchemesMonitor; +static NSSet *RNCachingSupportedSchemes; + +@implementation RNCachingURLProtocol +@synthesize connection = connection_; +@synthesize data = data_; +@synthesize response = response_; + ++ (void)initialize +{ + if (self == [RNCachingURLProtocol class]) + { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + RNCachingSupportedSchemesMonitor = [NSObject new]; + }); + + [self setSupportedSchemes:[NSSet setWithObject:@"http"]]; + } +} + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + // only handle http requests we haven't marked with our header. + if ([[self supportedSchemes] containsObject:[[request URL] scheme]] && + ([request valueForHTTPHeaderField:RNCachingURLHeader] == nil)) + { + return YES; + } + return NO; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + +- (NSString *)cachePathForRequest:(NSURLRequest *)aRequest +{ + // This stores in the Caches directory, which can be deleted when space is low, but we only use it for offline access + NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; + NSString *fileName = [[[aRequest URL] absoluteString] sha1]; + + return [cachesPath stringByAppendingPathComponent:fileName]; +} + +- (void)startLoading +{ + if (![self useCache]) { + NSMutableURLRequest *connectionRequest = +#if WORKAROUND_MUTABLE_COPY_LEAK + [[self request] mutableCopyWorkaround]; +#else + [[self request] mutableCopy]; +#endif + // we need to mark this request with our header so we know not to handle it in +[NSURLProtocol canInitWithRequest:]. + [connectionRequest setValue:@"" forHTTPHeaderField:RNCachingURLHeader]; + NSURLConnection *connection = [NSURLConnection connectionWithRequest:connectionRequest + delegate:self]; + [self setConnection:connection]; + } + else { + RNCachedData *cache = [NSKeyedUnarchiver unarchiveObjectWithFile:[self cachePathForRequest:[self request]]]; + if (cache) { + NSData *data = [cache data]; + NSURLResponse *response = [cache response]; + NSURLRequest *redirectRequest = [cache redirectRequest]; + if (redirectRequest) { + [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response]; + } else { + + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; // we handle caching ourselves. + [[self client] URLProtocol:self didLoadData:data]; + [[self client] URLProtocolDidFinishLoading:self]; + } + } + else { + [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotConnectToHost userInfo:nil]]; + } + } +} + +- (void)stopLoading +{ + [[self connection] cancel]; +} + +// NSURLConnection delegates (generally we pass these on to our client) + +- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response +{ +// Thanks to Nick Dowell https://gist.github.com/1885821 + if (response != nil) { + NSMutableURLRequest *redirectableRequest = +#if WORKAROUND_MUTABLE_COPY_LEAK + [request mutableCopyWorkaround]; +#else + [request mutableCopy]; +#endif + // We need to remove our header so we know to handle this request and cache it. + // There are 3 requests in flight: the outside request, which we handled, the internal request, + // which we marked with our header, and the redirectableRequest, which we're modifying here. + // The redirectable request will cause a new outside request from the NSURLProtocolClient, which + // must not be marked with our header. + [redirectableRequest setValue:nil forHTTPHeaderField:RNCachingURLHeader]; + + NSString *cachePath = [self cachePathForRequest:[self request]]; + RNCachedData *cache = [RNCachedData new]; + [cache setResponse:response]; + [cache setData:[self data]]; + [cache setRedirectRequest:redirectableRequest]; + [NSKeyedArchiver archiveRootObject:cache toFile:cachePath]; + [[self client] URLProtocol:self wasRedirectedToRequest:redirectableRequest redirectResponse:response]; + return redirectableRequest; + } else { + return request; + } +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data +{ + [[self client] URLProtocol:self didLoadData:data]; + [self appendData:data]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + [[self client] URLProtocol:self didFailWithError:error]; + [self setConnection:nil]; + [self setData:nil]; + [self setResponse:nil]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response +{ + [self setResponse:response]; + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; // We cache ourselves. +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + [[self client] URLProtocolDidFinishLoading:self]; + + NSString *cachePath = [self cachePathForRequest:[self request]]; + RNCachedData *cache = [RNCachedData new]; + [cache setResponse:[self response]]; + [cache setData:[self data]]; + [NSKeyedArchiver archiveRootObject:cache toFile:cachePath]; + + [self setConnection:nil]; + [self setData:nil]; + [self setResponse:nil]; +} + +- (BOOL) useCache +{ + BOOL reachable = (BOOL) [[Reachability reachabilityWithHostName:[[[self request] URL] host]] currentReachabilityStatus] != NotReachable; + return !reachable; +} + +- (void)appendData:(NSData *)newData +{ + if ([self data] == nil) { + [self setData:[newData mutableCopy]]; + } + else { + [[self data] appendData:newData]; + } +} + ++ (NSSet *)supportedSchemes { + NSSet *supportedSchemes; + @synchronized(RNCachingSupportedSchemesMonitor) + { + supportedSchemes = RNCachingSupportedSchemes; + } + return supportedSchemes; +} + ++ (void)setSupportedSchemes:(NSSet *)supportedSchemes +{ + @synchronized(RNCachingSupportedSchemesMonitor) + { + RNCachingSupportedSchemes = supportedSchemes; + } +} + +@end + +static NSString *const kDataKey = @"data"; +static NSString *const kResponseKey = @"response"; +static NSString *const kRedirectRequestKey = @"redirectRequest"; + +@implementation RNCachedData +@synthesize data = data_; +@synthesize response = response_; +@synthesize redirectRequest = redirectRequest_; + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:[self data] forKey:kDataKey]; + [aCoder encodeObject:[self response] forKey:kResponseKey]; + [aCoder encodeObject:[self redirectRequest] forKey:kRedirectRequestKey]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) { + [self setData:[aDecoder decodeObjectForKey:kDataKey]]; + [self setResponse:[aDecoder decodeObjectForKey:kResponseKey]]; + [self setRedirectRequest:[aDecoder decodeObjectForKey:kRedirectRequestKey]]; + } + + return self; +} + +@end + +#if WORKAROUND_MUTABLE_COPY_LEAK +@implementation NSURLRequest(MutableCopyWorkaround) + +- (id) mutableCopyWorkaround { + NSMutableURLRequest *mutableURLRequest = [[NSMutableURLRequest alloc] initWithURL:[self URL] + cachePolicy:[self cachePolicy] + timeoutInterval:[self timeoutInterval]]; + [mutableURLRequest setAllHTTPHeaderFields:[self allHTTPHeaderFields]]; + if ([self HTTPBodyStream]) { + [mutableURLRequest setHTTPBodyStream:[self HTTPBodyStream]]; + } else { + [mutableURLRequest setHTTPBody:[self HTTPBody]]; + } + [mutableURLRequest setHTTPMethod:[self HTTPMethod]]; + + return mutableURLRequest; +} + +@end +#endif diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/1024.png b/msext/Images.xcassets/AppIcon-1.appiconset/1024.png new file mode 100755 index 0000000..ad0e962 Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/1024.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/29.png b/msext/Images.xcassets/AppIcon-1.appiconset/29.png new file mode 100755 index 0000000..41ba2f5 Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/29.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/57.png b/msext/Images.xcassets/AppIcon-1.appiconset/57.png new file mode 100755 index 0000000..22bfe79 Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/57.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Contents.json b/msext/Images.xcassets/AppIcon-1.appiconset/Contents.json new file mode 100755 index 0000000..7102e43 --- /dev/null +++ b/msext/Images.xcassets/AppIcon-1.appiconset/Contents.json @@ -0,0 +1,78 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon40@3x.png", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "57.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "icon57@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon40@3x-1.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon60@3x.png", + "scale" : "3x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "1024.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Icon29@2x.png b/msext/Images.xcassets/AppIcon-1.appiconset/Icon29@2x.png new file mode 100755 index 0000000..904f9bf Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/Icon29@2x.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Icon29@3x.png b/msext/Images.xcassets/AppIcon-1.appiconset/Icon29@3x.png new file mode 100755 index 0000000..657fdc8 Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/Icon29@3x.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@2x.png b/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@2x.png new file mode 100755 index 0000000..c56c1d6 Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@2x.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@3x-1.png b/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@3x-1.png new file mode 100755 index 0000000..a814abb Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@3x-1.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@3x.png b/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@3x.png new file mode 100755 index 0000000..a814abb Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/Icon40@3x.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/Icon60@3x.png b/msext/Images.xcassets/AppIcon-1.appiconset/Icon60@3x.png new file mode 100755 index 0000000..b597cf7 Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/Icon60@3x.png differ diff --git a/msext/Images.xcassets/AppIcon-1.appiconset/icon57@2x.png b/msext/Images.xcassets/AppIcon-1.appiconset/icon57@2x.png new file mode 100755 index 0000000..2a0e01b Binary files /dev/null and b/msext/Images.xcassets/AppIcon-1.appiconset/icon57@2x.png differ diff --git a/msext/Images.xcassets/AppIcon.appiconset/Contents.json b/msext/Images.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 118c98f..0000000 --- a/msext/Images.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/msext/Images.xcassets/Contents.json b/msext/Images.xcassets/Contents.json new file mode 100755 index 0000000..da4a164 --- /dev/null +++ b/msext/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/1242×2208.png b/msext/Images.xcassets/LaunchImage-1.launchimage/1242×2208.png new file mode 100755 index 0000000..4a4fe93 Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/1242×2208.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/2436.png b/msext/Images.xcassets/LaunchImage-1.launchimage/2436.png new file mode 100755 index 0000000..bfff70b Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/2436.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/320×480.png b/msext/Images.xcassets/LaunchImage-1.launchimage/320×480.png new file mode 100755 index 0000000..409bdfa Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/320×480.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/750×1334.png b/msext/Images.xcassets/LaunchImage-1.launchimage/750×1334.png new file mode 100755 index 0000000..31bf2c7 Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/750×1334.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/Contents.json b/msext/Images.xcassets/LaunchImage-1.launchimage/Contents.json new file mode 100755 index 0000000..3efd487 --- /dev/null +++ b/msext/Images.xcassets/LaunchImage-1.launchimage/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "2436h", + "filename" : "2436.png", + "minimum-system-version" : "11.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "1242×2208.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "orientation" : "landscape", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "8.0", + "subtype" : "736h", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "750×1334.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default@2x~iphone-1.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "Default-568h@2x~iphone-1.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "320×480.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default@2x~iphone.png", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x~iphone.png", + "extent" : "full-screen", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/Default-568h@2x~iphone-1.png b/msext/Images.xcassets/LaunchImage-1.launchimage/Default-568h@2x~iphone-1.png new file mode 100755 index 0000000..e79e0d6 Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/Default-568h@2x~iphone-1.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/Default-568h@2x~iphone.png b/msext/Images.xcassets/LaunchImage-1.launchimage/Default-568h@2x~iphone.png new file mode 100755 index 0000000..e79e0d6 Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/Default-568h@2x~iphone.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/Default@2x~iphone-1.png b/msext/Images.xcassets/LaunchImage-1.launchimage/Default@2x~iphone-1.png new file mode 100755 index 0000000..2851ef0 Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/Default@2x~iphone-1.png differ diff --git a/msext/Images.xcassets/LaunchImage-1.launchimage/Default@2x~iphone.png b/msext/Images.xcassets/LaunchImage-1.launchimage/Default@2x~iphone.png new file mode 100755 index 0000000..2851ef0 Binary files /dev/null and b/msext/Images.xcassets/LaunchImage-1.launchimage/Default@2x~iphone.png differ diff --git a/msext/Info.plist b/msext/Info.plist old mode 100644 new mode 100755 index 9d3b9ce..a345d81 --- a/msext/Info.plist +++ b/msext/Info.plist @@ -4,10 +4,12 @@ CFBundleDevelopmentRegion en + CFBundleDisplayName + 开元棋牌 CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.chao.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -15,26 +17,83 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.7 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + weixin + CFBundleURLSchemes + + wxa5fcff77c40d721c + + + + CFBundleTypeRole + Editor + CFBundleURLName + alipay + CFBundleURLSchemes + + wx53b7b1be084f3e56 + + + + CFBundleTypeRole + Editor + CFBundleURLName + xianliao + CFBundleURLSchemes + + xianliaoU1jJq3wgWluyB660 + + + CFBundleVersion - 1 + 1.7 + LSApplicationQueriesSchemes + + xianliao + alipayqr + alipay + alipayshare + alipays + wechat + weixin + LSRequiresIPhoneOS - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + 需要您的同意 + NSLocationUsageDescription + 应用需要获得您的位置,显示在游戏中,这样就可以看到玩家所在的区域! + NSLocationWhenInUseUsageDescription + 应用需要获得您的位置,显示在游戏中,这样就可以看到玩家所在的区域! + NSMicrophoneUsageDescription + 需要您的同意 + NSPhotoLibraryUsageDescription + 需要您的同意 + UIBackgroundModes + UIRequiredDeviceCapabilities armv7 + UIStatusBarHidden + UISupportedInterfaceOrientations - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft diff --git a/msext/PushConfig.plist b/msext/PushConfig.plist new file mode 100755 index 0000000..aae5684 --- /dev/null +++ b/msext/PushConfig.plist @@ -0,0 +1,12 @@ + + + + + APS_FOR_PRODUCTION + 0 + APP_KEY + e68ab6a0b8ae471fa9f26a6e + CHANNEL + Publish channel + + diff --git a/msext/QiniuSDK/Common/QNALAssetFile.h b/msext/QiniuSDK/Common/QNALAssetFile.h new file mode 100755 index 0000000..2438af4 --- /dev/null +++ b/msext/QiniuSDK/Common/QNALAssetFile.h @@ -0,0 +1,28 @@ +// +// QNALAssetFile.h +// QiniuSDK +// +// Created by bailong on 15/7/25. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import + +#import "QNFileDelegate.h" + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) +@class ALAsset; +@interface QNALAssetFile : NSObject + +/** + * 打开指定文件 + * + * @param path 文件路径 + * @param error 输出的错误信息 + * + * @return 实例 + */ +- (instancetype)init:(ALAsset *)asset + error:(NSError *__autoreleasing *)error; +@end +#endif \ No newline at end of file diff --git a/msext/QiniuSDK/Common/QNALAssetFile.m b/msext/QiniuSDK/Common/QNALAssetFile.m new file mode 100755 index 0000000..af9408d --- /dev/null +++ b/msext/QiniuSDK/Common/QNALAssetFile.m @@ -0,0 +1,72 @@ +// +// QNALAssetFile.m +// QiniuSDK +// +// Created by bailong on 15/7/25. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNALAssetFile.h" + +#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED +#import + +#import "QNResponseInfo.h" + +@interface QNALAssetFile () + +@property (nonatomic) ALAsset *asset; + +@property (readonly) int64_t fileSize; + +@property (readonly) int64_t fileModifyTime; + +@end + +@implementation QNALAssetFile +- (instancetype)init:(ALAsset *)asset + error:(NSError *__autoreleasing *)error { + if (self = [super init]) { + NSDate *createTime = [asset valueForProperty:ALAssetPropertyDate]; + int64_t t = 0; + if (createTime != nil) { + t = [createTime timeIntervalSince1970]; + } + _fileModifyTime = t; + _fileSize = asset.defaultRepresentation.size; + _asset = asset; + } + + return self; +} + +- (NSData *)read:(long)offset + size:(long)size { + ALAssetRepresentation *rep = [self.asset defaultRepresentation]; + Byte *buffer = (Byte *)malloc(size); + NSUInteger buffered = [rep getBytes:buffer fromOffset:offset length:size error:nil]; + + return [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; +} + +- (NSData *)readAll { + return [self read:0 size:(long)_fileSize]; +} + +- (void)close { +} + +- (NSString *)path { + ALAssetRepresentation *rep = [self.asset defaultRepresentation]; + return [rep url].path; +} + +- (int64_t)modifyTime { + return _fileModifyTime; +} + +- (int64_t)size { + return _fileSize; +} +@end +#endif diff --git a/msext/QiniuSDK/Common/QNAsyncRun.h b/msext/QiniuSDK/Common/QNAsyncRun.h new file mode 100755 index 0000000..8af4661 --- /dev/null +++ b/msext/QiniuSDK/Common/QNAsyncRun.h @@ -0,0 +1,13 @@ +// +// QNAsyncRun.h +// QiniuSDK +// +// Created by bailong on 14/10/17. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +typedef void (^QNRun)(void); + +void QNAsyncRun(QNRun run); + +void QNAsyncRunInMain(QNRun run); diff --git a/msext/QiniuSDK/Common/QNAsyncRun.m b/msext/QiniuSDK/Common/QNAsyncRun.m new file mode 100755 index 0000000..ea745de --- /dev/null +++ b/msext/QiniuSDK/Common/QNAsyncRun.m @@ -0,0 +1,22 @@ +// +// QNAsyncRun.m +// QiniuSDK +// +// Created by bailong on 14/10/17. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNAsyncRun.h" +#import + +void QNAsyncRun(QNRun run) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + run(); + }); +} + +void QNAsyncRunInMain(QNRun run) { + dispatch_async(dispatch_get_main_queue(), ^(void) { + run(); + }); +} diff --git a/msext/QiniuSDK/Common/QNCrc32.h b/msext/QiniuSDK/Common/QNCrc32.h new file mode 100755 index 0000000..e6d90ab --- /dev/null +++ b/msext/QiniuSDK/Common/QNCrc32.h @@ -0,0 +1,36 @@ +// +// QNCrc.h +// QiniuSDK +// +// Created by bailong on 14-9-29. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * 生成crc32 校验码 + */ +@interface QNCrc32 : NSObject + +/** + * 文件校验 + * + * @param filePath 文件路径 + * @param error 文件读取错误 + * + * @return 校验码 + */ ++ (UInt32)file:(NSString *)filePath + error:(NSError **)error; + +/** + * 二进制字节校验 + * + * @param data 二进制数据 + * + * @return 校验码 + */ ++ (UInt32)data:(NSData *)data; + +@end diff --git a/msext/QiniuSDK/Common/QNCrc32.m b/msext/QiniuSDK/Common/QNCrc32.m new file mode 100755 index 0000000..2632fc7 --- /dev/null +++ b/msext/QiniuSDK/Common/QNCrc32.m @@ -0,0 +1,45 @@ +// +// QNCrc.m +// QiniuSDK +// +// Created by bailong on 14-9-29. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +#import "QNConfiguration.h" +#import "QNCrc32.h" + +@implementation QNCrc32 + ++ (UInt32)data:(NSData *)data { + uLong crc = crc32(0L, Z_NULL, 0); + + crc = crc32(crc, [data bytes], (uInt)[data length]); + return (UInt32)crc; +} + ++ (UInt32)file:(NSString *)filePath + error:(NSError **)error { + @autoreleasepool { + NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:error]; + if (*error != nil) { + return 0; + } + + int len = (int)[data length]; + int count = (len + kQNBlockSize - 1) / kQNBlockSize; + + uLong crc = crc32(0L, Z_NULL, 0); + for (int i = 0; i < count; i++) { + int offset = i * kQNBlockSize; + int size = (len - offset) > kQNBlockSize ? kQNBlockSize : (len - offset); + NSData *d = [data subdataWithRange:NSMakeRange(offset, (unsigned int)size)]; + crc = crc32(crc, [d bytes], (uInt)[d length]); + } + return (UInt32)crc; + } +} + +@end diff --git a/msext/QiniuSDK/Common/QNEtag.h b/msext/QiniuSDK/Common/QNEtag.h new file mode 100755 index 0000000..e1e1d7e --- /dev/null +++ b/msext/QiniuSDK/Common/QNEtag.h @@ -0,0 +1,35 @@ +// +// QNEtag.h +// QiniuSDK +// +// Created by bailong on 14/10/4. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * 服务器 hash etag 生成 + */ +@interface QNEtag : NSObject + +/** + * 文件etag + * + * @param filePath 文件路径 + * @param error 输出文件读取错误 + * + * @return etag + */ ++ (NSString *)file:(NSString *)filePath + error:(NSError **)error; + +/** + * 二进制数据etag + * + * @param data 数据 + * + * @return etag + */ ++ (NSString *)data:(NSData *)data; +@end diff --git a/msext/QiniuSDK/Common/QNEtag.m b/msext/QiniuSDK/Common/QNEtag.m new file mode 100755 index 0000000..36ecfe8 --- /dev/null +++ b/msext/QiniuSDK/Common/QNEtag.m @@ -0,0 +1,60 @@ +// +// QNEtag.m +// QiniuSDK +// +// Created by bailong on 14/10/4. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#include + +#import "QNConfiguration.h" +#import "QNEtag.h" +#import "QNUrlSafeBase64.h" + +@implementation QNEtag ++ (NSString *)file:(NSString *)filePath + error:(NSError **)error { + @autoreleasepool { + NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:error]; + if (*error != nil) { + return 0; + } + return [QNEtag data:data]; + } +} + ++ (NSString *)data:(NSData *)data { + if (data == nil || [data length] == 0) { + return @"Fto5o-5ea0sNMlW_75VgGJCv2AcJ"; + } + int len = (int)[data length]; + int count = (len + kQNBlockSize - 1) / kQNBlockSize; + + NSMutableData *retData = [NSMutableData dataWithLength:CC_SHA1_DIGEST_LENGTH + 1]; + UInt8 *ret = [retData mutableBytes]; + + NSMutableData *blocksSha1 = nil; + UInt8 *pblocksSha1 = ret + 1; + if (count > 1) { + blocksSha1 = [NSMutableData dataWithLength:CC_SHA1_DIGEST_LENGTH * count]; + pblocksSha1 = [blocksSha1 mutableBytes]; + } + + for (int i = 0; i < count; i++) { + int offset = i * kQNBlockSize; + int size = (len - offset) > kQNBlockSize ? kQNBlockSize : (len - offset); + NSData *d = [data subdataWithRange:NSMakeRange(offset, (unsigned int)size)]; + CC_SHA1([d bytes], (CC_LONG)size, pblocksSha1 + i * CC_SHA1_DIGEST_LENGTH); + } + if (count == 1) { + ret[0] = 0x16; + } else { + ret[0] = 0x96; + CC_SHA1(pblocksSha1, (CC_LONG)CC_SHA1_DIGEST_LENGTH * count, ret + 1); + } + + return [QNUrlSafeBase64 encodeData:retData]; +} + +@end diff --git a/msext/QiniuSDK/Common/QNFile.h b/msext/QiniuSDK/Common/QNFile.h new file mode 100755 index 0000000..4cabf37 --- /dev/null +++ b/msext/QiniuSDK/Common/QNFile.h @@ -0,0 +1,24 @@ +// +// QNFile.h +// QiniuSDK +// +// Created by bailong on 15/7/25. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNFileDelegate.h" +#import + +@interface QNFile : NSObject +/** + * 打开指定文件 + * + * @param path 文件路径 + * @param error 输出的错误信息 + * + * @return 实例 + */ +- (instancetype)init:(NSString *)path + error:(NSError *__autoreleasing *)error; + +@end diff --git a/msext/QiniuSDK/Common/QNFile.m b/msext/QiniuSDK/Common/QNFile.m new file mode 100755 index 0000000..255a9d3 --- /dev/null +++ b/msext/QiniuSDK/Common/QNFile.m @@ -0,0 +1,106 @@ +// +// QNFile.m +// QiniuSDK +// +// Created by bailong on 15/7/25. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNFile.h" +#import "QNResponseInfo.h" + +@interface QNFile () + +@property (nonatomic, readonly) NSString *filepath; + +@property (nonatomic) NSData *data; + +@property (readonly) int64_t fileSize; + +@property (readonly) int64_t fileModifyTime; + +@property (nonatomic) NSFileHandle *file; + +@end + +@implementation QNFile + +- (instancetype)init:(NSString *)path + error:(NSError *__autoreleasing *)error { + if (self = [super init]) { + _filepath = path; + NSError *error2 = nil; + NSDictionary *fileAttr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error2]; + if (error2 != nil) { + if (error != nil) { + *error = error2; + } + return self; + } + _fileSize = [fileAttr fileSize]; + NSDate *modifyTime = fileAttr[NSFileModificationDate]; + int64_t t = 0; + if (modifyTime != nil) { + t = [modifyTime timeIntervalSince1970]; + } + _fileModifyTime = t; + NSFileHandle *f = nil; + NSData *d = nil; + //[NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error] 不能用在大于 200M的文件上,改用filehandle + // 参见 https://issues.apache.org/jira/browse/CB-5790 + if (_fileSize > 16 * 1024 * 1024) { + f = [NSFileHandle fileHandleForReadingAtPath:path]; + if (f == nil) { + if (error != nil) { + *error = [[NSError alloc] initWithDomain:path code:kQNFileError userInfo:nil]; + } + return self; + } + } else { + d = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error2]; + if (error2 != nil) { + if (error != nil) { + *error = error2; + } + return self; + } + } + _file = f; + _data = d; + } + + return self; +} + +- (NSData *)read:(long)offset + size:(long)size { + if (_data != nil) { + return [_data subdataWithRange:NSMakeRange(offset, (unsigned int)size)]; + } + [_file seekToFileOffset:offset]; + return [_file readDataOfLength:size]; +} + +- (NSData *)readAll { + return [self read:0 size:(long)_fileSize]; +} + +- (void)close { + if (_file != nil) { + [_file closeFile]; + } +} + +- (NSString *)path { + return _filepath; +} + +- (int64_t)modifyTime { + return _fileModifyTime; +} + +- (int64_t)size { + return _fileSize; +} + +@end diff --git a/msext/QiniuSDK/Common/QNFileDelegate.h b/msext/QiniuSDK/Common/QNFileDelegate.h new file mode 100755 index 0000000..e6cbeb8 --- /dev/null +++ b/msext/QiniuSDK/Common/QNFileDelegate.h @@ -0,0 +1,61 @@ +// +// QNFileDelegate.h +// QiniuSDK +// +// Created by bailong on 15/7/25. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import + +/** + * 文件处理接口,支持ALAsset, NSFileHandle, NSData + */ +@protocol QNFileDelegate + +/** + * 从指定偏移读取数据 + * + * @param offset 偏移地址 + * @param size 大小 + * + * @return 数据 + */ +- (NSData *)read:(long)offset + size:(long)size; + +/** + * 读取所有文件内容 + * + * @return 数据 + */ +- (NSData *)readAll; + +/** + * 关闭文件 + * + */ +- (void)close; + +/** + * 文件路径 + * + * @return 文件路径 + */ +- (NSString *)path; + +/** + * 文件修改时间 + * + * @return 修改时间 + */ +- (int64_t)modifyTime; + +/** + * 文件大小 + * + * @return 文件大小 + */ +- (int64_t)size; + +@end diff --git a/msext/QiniuSDK/Common/QNPHAssetFile.h b/msext/QiniuSDK/Common/QNPHAssetFile.h new file mode 100755 index 0000000..0ac6293 --- /dev/null +++ b/msext/QiniuSDK/Common/QNPHAssetFile.h @@ -0,0 +1,27 @@ +// +// QNPHAssetFile.h +// Pods +// +// Created by 何舒 on 15/10/21. +// +// + +#import + +#import "QNFileDelegate.h" + +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000) +@class PHAsset; +@interface QNPHAssetFile : NSObject +/** + * 打开指定文件 + * + * @param path 文件路径 + * @param error 输出的错误信息 + * + * @return 实例 + */ +- (instancetype)init:(PHAsset *)phAsset + error:(NSError *__autoreleasing *)error; +@end +#endif diff --git a/msext/QiniuSDK/Common/QNPHAssetFile.m b/msext/QiniuSDK/Common/QNPHAssetFile.m new file mode 100755 index 0000000..4285781 --- /dev/null +++ b/msext/QiniuSDK/Common/QNPHAssetFile.m @@ -0,0 +1,165 @@ +// +// QNPHAssetFile.m +// Pods +// +// Created by 何舒 on 15/10/21. +// +// + +#import "QNPHAssetFile.h" + +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000) +#import +#import + +#import "QNResponseInfo.h" + +@interface QNPHAssetFile () + +@property (nonatomic) PHAsset *phAsset; + +@property (readonly) int64_t fileSize; + +@property (readonly) int64_t fileModifyTime; + +@property (nonatomic, strong) NSData *assetData; + +@property (nonatomic, strong) NSURL *assetURL; + +@property (nonatomic, readonly) NSString *filepath; + +@property (nonatomic) NSFileHandle *file; + +@end + +@implementation QNPHAssetFile + +- (instancetype)init:(PHAsset *)phAsset error:(NSError *__autoreleasing *)error { + if (self = [super init]) { + NSDate *createTime = phAsset.creationDate; + int64_t t = 0; + if (createTime != nil) { + t = [createTime timeIntervalSince1970]; + } + _fileModifyTime = t; + _phAsset = phAsset; + _filepath = [self getInfo]; + if (PHAssetMediaTypeVideo == self.phAsset.mediaType) { + NSError *error2 = nil; + NSDictionary *fileAttr = [[NSFileManager defaultManager] attributesOfItemAtPath:_filepath error:&error2]; + if (error2 != nil) { + if (error != nil) { + *error = error2; + } + return self; + } + _fileSize = [fileAttr fileSize]; + NSFileHandle *f = nil; + NSData *d = nil; + if (_fileSize > 16 * 1024 * 1024) { + f = [NSFileHandle fileHandleForReadingAtPath:_filepath]; + if (f == nil) { + if (error != nil) { + *error = [[NSError alloc] initWithDomain:_filepath code:kQNFileError userInfo:nil]; + } + return self; + } + } else { + d = [NSData dataWithContentsOfFile:_filepath options:NSDataReadingMappedIfSafe error:&error2]; + if (error2 != nil) { + if (error != nil) { + *error = error2; + } + return self; + } + } + _file = f; + _assetData = d; + } + } + return self; +} + +- (NSData *)read:(long)offset size:(long)size { + if (_assetData != nil) { + return [_assetData subdataWithRange:NSMakeRange(offset, (unsigned int)size)]; + } + [_file seekToFileOffset:offset]; + return [_file readDataOfLength:size]; +} + +- (NSData *)readAll { + return [self read:0 size:(long)_fileSize]; +} + +- (void)close { + if (PHAssetMediaTypeVideo == self.phAsset.mediaType) { + if (_file != nil) { + [_file closeFile]; + } + [[NSFileManager defaultManager] removeItemAtPath:_filepath error:nil]; + } +} + +- (NSString *)path { + return _filepath; +} + +- (int64_t)modifyTime { + return _fileModifyTime; +} + +- (int64_t)size { + return _fileSize; +} + +- (NSString *)getInfo { + __block NSString *filePath = nil; + if (PHAssetMediaTypeImage == self.phAsset.mediaType) { + PHImageRequestOptions *options = [PHImageRequestOptions new]; + options.version = PHImageRequestOptionsVersionCurrent; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + //不支持icloud上传 + options.networkAccessAllowed = NO; + options.synchronous = YES; + + [[PHImageManager defaultManager] requestImageDataForAsset:self.phAsset + options:options + resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + _assetData = imageData; + _fileSize = imageData.length; + _assetURL = [NSURL URLWithString:self.phAsset.localIdentifier]; + filePath = _assetURL.path; + }]; + } else if (PHAssetMediaTypeVideo == self.phAsset.mediaType) { + NSArray *assetResources = [PHAssetResource assetResourcesForAsset:self.phAsset]; + PHAssetResource *resource; + for (PHAssetResource *assetRes in assetResources) { + if (assetRes.type == PHAssetResourceTypePairedVideo || assetRes.type == PHAssetResourceTypeVideo) { + resource = assetRes; + } + } + NSString *fileName = @"tempAssetVideo.mov"; + if (resource.originalFilename) { + fileName = resource.originalFilename; + } + PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new]; + //不支持icloud上传 + options.networkAccessAllowed = NO; + + NSString *PATH_VIDEO_FILE = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; + [[NSFileManager defaultManager] removeItemAtPath:PATH_VIDEO_FILE error:nil]; + [[PHAssetResourceManager defaultManager] writeDataForAssetResource:resource toFile:[NSURL fileURLWithPath:PATH_VIDEO_FILE] options:options completionHandler:^(NSError *_Nullable error) { + if (error) { + filePath = nil; + } else { + filePath = PATH_VIDEO_FILE; + } + }]; + } + return filePath; +} + +@end +#endif diff --git a/msext/QiniuSDK/Common/QNPHAssetResource.h b/msext/QiniuSDK/Common/QNPHAssetResource.h new file mode 100755 index 0000000..709de63 --- /dev/null +++ b/msext/QiniuSDK/Common/QNPHAssetResource.h @@ -0,0 +1,31 @@ +// +// QNPHAssetResource.h +// QiniuSDK +// +// Created by 何舒 on 16/2/14. +// Copyright © 2016年 Qiniu. All rights reserved. +// + +#import + +#import "QNFileDelegate.h" + +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90100) + +@class PHAssetResource; + +@interface QNPHAssetResource : NSObject + +/** + * 打开指定文件 + * + * @param path PHLivePhoto的PHAssetResource文件 + * @param error 输出的错误信息 + * + * @return 实例 + */ +- (instancetype)init:(PHAssetResource *)phAssetResource + error:(NSError *__autoreleasing *)error; + +@end +#endif diff --git a/msext/QiniuSDK/Common/QNPHAssetResource.m b/msext/QiniuSDK/Common/QNPHAssetResource.m new file mode 100755 index 0000000..d0458a0 --- /dev/null +++ b/msext/QiniuSDK/Common/QNPHAssetResource.m @@ -0,0 +1,175 @@ +// +// QNPHAssetResource.m +// QiniuSDK +// +// Created by 何舒 on 16/2/14. +// Copyright © 2016年 Qiniu. All rights reserved. +// + +#import "QNPHAssetResource.h" +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90100) +#import +#import + +enum { + kAMASSETMETADATA_PENDINGREADS = 1, + kAMASSETMETADATA_ALLFINISHED = 0 +}; + +#import "QNResponseInfo.h" + +@interface QNPHAssetResource () + + { + BOOL _hasGotInfo; +} + +@property (nonatomic) PHAsset *phAsset; + +@property (nonatomic) PHLivePhoto *phLivePhoto; + +@property (nonatomic) PHAssetResource *phAssetResource; + +@property (readonly) int64_t fileSize; + +@property (readonly) int64_t fileModifyTime; + +@property (nonatomic, strong) NSData *assetData; + +@property (nonatomic, strong) NSURL *assetURL; + +@end + +@implementation QNPHAssetResource +- (instancetype)init:(PHAssetResource *)phAssetResource + error:(NSError *__autoreleasing *)error { + if (self = [super init]) { + PHAsset *phasset = [PHAsset fetchAssetsWithBurstIdentifier:self.phAssetResource.assetLocalIdentifier options:nil][0]; + NSDate *createTime = phasset.creationDate; + int64_t t = 0; + if (createTime != nil) { + t = [createTime timeIntervalSince1970]; + } + _fileModifyTime = t; + _phAssetResource = phAssetResource; + [self getInfo]; + } + return self; +} + +- (NSData *)read:(long)offset size:(long)size { + NSRange subRange = NSMakeRange(offset, size); + if (!self.assetData) { + self.assetData = [self fetchDataFromAsset:self.phAssetResource]; + } + NSData *subData = [self.assetData subdataWithRange:subRange]; + + return subData; +} + +- (NSData *)readAll { + return [self read:0 size:(long)_fileSize]; +} + +- (void)close { +} + +- (NSString *)path { + return self.assetURL.path; +} + +- (int64_t)modifyTime { + return _fileModifyTime; +} + +- (int64_t)size { + return _fileSize; +} + +- (void)getInfo { + if (!_hasGotInfo) { + _hasGotInfo = YES; + NSConditionLock *assetReadLock = [[NSConditionLock alloc] initWithCondition:kAMASSETMETADATA_PENDINGREADS]; + + NSString *pathToWrite = [NSTemporaryDirectory() stringByAppendingString:self.phAssetResource.originalFilename]; + NSURL *localpath = [NSURL fileURLWithPath:pathToWrite]; + PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new]; + options.networkAccessAllowed = YES; + [[PHAssetResourceManager defaultManager] writeDataForAssetResource:self.phAssetResource toFile:localpath options:options completionHandler:^(NSError *_Nullable error) { + if (error == nil) { + AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:localpath options:nil]; + NSNumber *fileSize = nil; + [urlAsset.URL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil]; + _fileSize = [fileSize unsignedLongLongValue]; + _assetURL = urlAsset.URL; + self.assetData = [NSData dataWithData:[NSData dataWithContentsOfURL:urlAsset.URL]]; + } else { + NSLog(@"%@", error); + } + + BOOL blHave = [[NSFileManager defaultManager] fileExistsAtPath:pathToWrite]; + if (!blHave) { + NSLog(@"no have"); + return; + } else { + NSLog(@" have"); + BOOL blDele = [[NSFileManager defaultManager] removeItemAtPath:pathToWrite error:nil]; + if (blDele) { + NSLog(@"dele success"); + } else { + NSLog(@"dele fail"); + } + } + [assetReadLock lock]; + [assetReadLock unlockWithCondition:kAMASSETMETADATA_ALLFINISHED]; + }]; + + [assetReadLock lockWhenCondition:kAMASSETMETADATA_ALLFINISHED]; + [assetReadLock unlock]; + assetReadLock = nil; + } +} + +- (NSData *)fetchDataFromAsset:(PHAssetResource *)videoResource { + __block NSData *tmpData = [NSData data]; + + NSConditionLock *assetReadLock = [[NSConditionLock alloc] initWithCondition:kAMASSETMETADATA_PENDINGREADS]; + + NSString *pathToWrite = [NSTemporaryDirectory() stringByAppendingString:videoResource.originalFilename]; + NSURL *localpath = [NSURL fileURLWithPath:pathToWrite]; + PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new]; + options.networkAccessAllowed = YES; + [[PHAssetResourceManager defaultManager] writeDataForAssetResource:videoResource toFile:localpath options:options completionHandler:^(NSError *_Nullable error) { + if (error == nil) { + AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:localpath options:nil]; + NSData *videoData = [NSData dataWithContentsOfURL:urlAsset.URL]; + tmpData = [NSData dataWithData:videoData]; + } else { + NSLog(@"%@", error); + } + BOOL blHave = [[NSFileManager defaultManager] fileExistsAtPath:pathToWrite]; + if (!blHave) { + NSLog(@"no have"); + return; + } else { + NSLog(@" have"); + BOOL blDele = [[NSFileManager defaultManager] removeItemAtPath:pathToWrite error:nil]; + if (blDele) { + NSLog(@"dele success"); + } else { + NSLog(@"dele fail"); + } + } + [assetReadLock lock]; + [assetReadLock unlockWithCondition:kAMASSETMETADATA_ALLFINISHED]; + }]; + + [assetReadLock lockWhenCondition:kAMASSETMETADATA_ALLFINISHED]; + [assetReadLock unlock]; + assetReadLock = nil; + + return tmpData; +} + +@end +#endif diff --git a/msext/QiniuSDK/Common/QNSystem.h b/msext/QiniuSDK/Common/QNSystem.h new file mode 100755 index 0000000..4c96c65 --- /dev/null +++ b/msext/QiniuSDK/Common/QNSystem.h @@ -0,0 +1,20 @@ +// +// QNSystem.h +// QiniuSDK +// +// Created by bailong on 15/10/13. +// Copyright © 2015年 Qiniu. All rights reserved. +// + +#ifndef QNSystem_h +#define QNSystem_h + +BOOL hasNSURLSession(); + +BOOL hasAts(); + +BOOL allowsArbitraryLoads(); + +BOOL isIpV6FullySupported(); + +#endif /* QNSystem_h */ diff --git a/msext/QiniuSDK/Common/QNSystem.m b/msext/QiniuSDK/Common/QNSystem.m new file mode 100755 index 0000000..ed5c403 --- /dev/null +++ b/msext/QiniuSDK/Common/QNSystem.m @@ -0,0 +1,89 @@ +// +// QNSystem.m +// QiniuSDK +// +// Created by bailong on 15/10/13. +// Copyright © 2015年 Qiniu. All rights reserved. +// + +#import + +#if __IPHONE_OS_VERSION_MIN_REQUIRED +#import +#import +#else +#import +#endif + +BOOL hasNSURLSession() { +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) + float sysVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; + if (sysVersion < 7.0) { + return NO; + } +#else + NSOperatingSystemVersion sysVersion = [[NSProcessInfo processInfo] operatingSystemVersion]; + if (sysVersion.majorVersion < 10) { + return NO; + } else if (sysVersion.majorVersion == 10) { + return sysVersion.minorVersion >= 9; + } +#endif + return YES; +} + +BOOL hasAts() { +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) + float sysVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; + if (sysVersion < 9.0) { + return NO; + } +#else + NSOperatingSystemVersion sysVersion = [[NSProcessInfo processInfo] operatingSystemVersion]; + if (sysVersion.majorVersion < 10) { + return NO; + } else if (sysVersion.majorVersion == 10) { + return sysVersion.minorVersion >= 11; + } +#endif + return YES; +} + +BOOL allowsArbitraryLoads() { + if (!hasAts()) { + return YES; + } + + // for unit test + NSDictionary* d = [[NSBundle mainBundle] infoDictionary]; + if (d == nil || d.count == 0) { + return YES; + } + + NSDictionary* sec = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + if (sec == nil) { + return NO; + } + NSNumber* ats = [sec objectForKey:@"NSAllowsArbitraryLoads"]; + if (ats == nil) { + return NO; + } + return ats.boolValue; +} + +BOOL isIpV6FullySupported() { +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) + float sysVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; + if (sysVersion < 9.0) { + return NO; + } +#else + NSOperatingSystemVersion sysVersion = [[NSProcessInfo processInfo] operatingSystemVersion]; + if (sysVersion.majorVersion < 10) { + return NO; + } else if (sysVersion.majorVersion == 10) { + return sysVersion.minorVersion >= 11; + } +#endif + return YES; +} diff --git a/msext/QiniuSDK/Common/QNUrlSafeBase64.h b/msext/QiniuSDK/Common/QNUrlSafeBase64.h new file mode 100755 index 0000000..8d5e6a5 --- /dev/null +++ b/msext/QiniuSDK/Common/QNUrlSafeBase64.h @@ -0,0 +1,41 @@ +// +// QiniuSDK +// +// Created by bailong on 14-9-28. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * url safe base64 编码类, 对/ 做了处理 + */ +@interface QNUrlSafeBase64 : NSObject + +/** + * 字符串编码 + * + * @param source 字符串 + * + * @return base64 字符串 + */ ++ (NSString *)encodeString:(NSString *)source; + +/** + * 二进制数据编码 + * + * @param source 二进制数据 + * + * @return base64字符串 + */ ++ (NSString *)encodeData:(NSData *)source; + +/** + * 字符串解码 + * + * @param base64 字符串 + * + * @return 数据 + */ ++ (NSData *)decodeString:(NSString *)data; +@end diff --git a/msext/QiniuSDK/Common/QNUrlSafeBase64.m b/msext/QiniuSDK/Common/QNUrlSafeBase64.m new file mode 100755 index 0000000..0079c24 --- /dev/null +++ b/msext/QiniuSDK/Common/QNUrlSafeBase64.m @@ -0,0 +1,29 @@ +// +// QiniuSDK +// +// Created by bailong on 14-9-28. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +#import "QNUrlSafeBase64.h" + +#import "QN_GTM_Base64.h" + +@implementation QNUrlSafeBase64 + ++ (NSString *)encodeString:(NSString *)sourceString { + NSData *data = [NSData dataWithBytes:[sourceString UTF8String] length:[sourceString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; + return [self encodeData:data]; +} + ++ (NSString *)encodeData:(NSData *)data { + return [QN_GTM_Base64 stringByWebSafeEncodingData:data padded:YES]; +} + ++ (NSData *)decodeString:(NSString *)data { + return [QN_GTM_Base64 webSafeDecodeString:data]; +} + +@end diff --git a/msext/QiniuSDK/Common/QNVersion.h b/msext/QiniuSDK/Common/QNVersion.h new file mode 100755 index 0000000..8b0a7e3 --- /dev/null +++ b/msext/QiniuSDK/Common/QNVersion.h @@ -0,0 +1,14 @@ +// +// QNVersion.h +// QiniuSDK +// +// Created by bailong on 14-9-29. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * sdk 版本 + */ +static const NSString *kQiniuVersion = @"7.1.5"; diff --git a/msext/QiniuSDK/Common/QN_GTM_Base64.h b/msext/QiniuSDK/Common/QN_GTM_Base64.h new file mode 100755 index 0000000..096a234 --- /dev/null +++ b/msext/QiniuSDK/Common/QN_GTM_Base64.h @@ -0,0 +1,182 @@ +// +// GTMBase64.h +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import + +// GTMBase64 +// +/// Helper for handling Base64 and WebSafeBase64 encodings +// +/// The webSafe methods use different character set and also the results aren't +/// always padded to a multiple of 4 characters. This is done so the resulting +/// data can be used in urls and url query arguments without needing any +/// encoding. You must use the webSafe* methods together, the data does not +/// interop with the RFC methods. +// +@interface QN_GTM_Base64 : NSObject + +// +// Standard Base64 (RFC) handling +// + +// encodeData: +// +/// Base64 encodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++ (NSData *)encodeData:(NSData *)data; + +// decodeData: +// +/// Base64 decodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++ (NSData *)decodeData:(NSData *)data; + +// encodeBytes:length: +// +/// Base64 encodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++ (NSData *)encodeBytes:(const void *)bytes length:(NSUInteger)length; + +// decodeBytes:length: +// +/// Base64 decodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++ (NSData *)decodeBytes:(const void *)bytes length:(NSUInteger)length; + +// stringByEncodingData: +// +/// Base64 encodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++ (NSString *)stringByEncodingData:(NSData *)data; + +// stringByEncodingBytes:length: +// +/// Base64 encodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++ (NSString *)stringByEncodingBytes:(const void *)bytes length:(NSUInteger)length; + +// decodeString: +// +/// Base64 decodes contents of the NSString. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++ (NSData *)decodeString:(NSString *)string; + +// +// Modified Base64 encoding so the results can go onto urls. +// +// The changes are in the characters generated and also allows the result to +// not be padded to a multiple of 4. +// Must use the matching call to encode/decode, won't interop with the +// RFC versions. +// + +// webSafeEncodeData:padded: +// +/// WebSafe Base64 encodes contents of the NSData object. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++ (NSData *)webSafeEncodeData:(NSData *)data + padded:(BOOL)padded; + +// webSafeDecodeData: +// +/// WebSafe Base64 decodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++ (NSData *)webSafeDecodeData:(NSData *)data; + +// webSafeEncodeBytes:length:padded: +// +/// WebSafe Base64 encodes the data pointed at by |bytes|. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++ (NSData *)webSafeEncodeBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded; + +// webSafeDecodeBytes:length: +// +/// WebSafe Base64 decodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++ (NSData *)webSafeDecodeBytes:(const void *)bytes length:(NSUInteger)length; + +// stringByWebSafeEncodingData:padded: +// +/// WebSafe Base64 encodes contents of the NSData object. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++ (NSString *)stringByWebSafeEncodingData:(NSData *)data + padded:(BOOL)padded; + +// stringByWebSafeEncodingBytes:length:padded: +// +/// WebSafe Base64 encodes the data pointed at by |bytes|. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++ (NSString *)stringByWebSafeEncodingBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded; + +// webSafeDecodeString: +// +/// WebSafe Base64 decodes contents of the NSString. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++ (NSData *)webSafeDecodeString:(NSString *)string; + +@end diff --git a/msext/QiniuSDK/Common/QN_GTM_Base64.m b/msext/QiniuSDK/Common/QN_GTM_Base64.m new file mode 100755 index 0000000..57edcc9 --- /dev/null +++ b/msext/QiniuSDK/Common/QN_GTM_Base64.m @@ -0,0 +1,693 @@ +// +// GTMBase64.m +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import "QN_GTM_Base64.h" + +static const char *kBase64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char *kWebSafeBase64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +static const char kBase64PaddingChar = '='; +static const char kBase64InvalidChar = 99; + +static const char kBase64DecodeChars[] = { + // This array was generated by the following code: + // #include + // #include + // #include + // main() + // { + // static const char Base64[] = + // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + // char *pos; + // int idx, i, j; + // printf(" "); + // for (i = 0; i < 255; i += 8) { + // for (j = i; j < i + 8; j++) { + // pos = strchr(Base64, j); + // if ((pos == NULL) || (j == 0)) + // idx = 99; + // else + // idx = pos - Base64; + // if (idx == 99) + // printf(" %2d, ", idx); + // else + // printf(" %2d/*%c*/,", idx, j); + // } + // printf("\n "); + // } + // } + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 62 /*+*/, 99, 99, 99, 63 /*/ */, + 52 /*0*/, 53 /*1*/, 54 /*2*/, 55 /*3*/, 56 /*4*/, 57 /*5*/, 58 /*6*/, 59 /*7*/, + 60 /*8*/, 61 /*9*/, 99, 99, 99, 99, 99, 99, + 99, 0 /*A*/, 1 /*B*/, 2 /*C*/, 3 /*D*/, 4 /*E*/, 5 /*F*/, 6 /*G*/, + 7 /*H*/, 8 /*I*/, 9 /*J*/, 10 /*K*/, 11 /*L*/, 12 /*M*/, 13 /*N*/, 14 /*O*/, + 15 /*P*/, 16 /*Q*/, 17 /*R*/, 18 /*S*/, 19 /*T*/, 20 /*U*/, 21 /*V*/, 22 /*W*/, + 23 /*X*/, 24 /*Y*/, 25 /*Z*/, 99, 99, 99, 99, 99, + 99, 26 /*a*/, 27 /*b*/, 28 /*c*/, 29 /*d*/, 30 /*e*/, 31 /*f*/, 32 /*g*/, + 33 /*h*/, 34 /*i*/, 35 /*j*/, 36 /*k*/, 37 /*l*/, 38 /*m*/, 39 /*n*/, 40 /*o*/, + 41 /*p*/, 42 /*q*/, 43 /*r*/, 44 /*s*/, 45 /*t*/, 46 /*u*/, 47 /*v*/, 48 /*w*/, + 49 /*x*/, 50 /*y*/, 51 /*z*/, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99}; + +static const char kWebSafeBase64DecodeChars[] = { + // This array was generated by the following code: + // #include + // #include + // #include + // main() + // { + // static const char Base64[] = + // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + // char *pos; + // int idx, i, j; + // printf(" "); + // for (i = 0; i < 255; i += 8) { + // for (j = i; j < i + 8; j++) { + // pos = strchr(Base64, j); + // if ((pos == NULL) || (j == 0)) + // idx = 99; + // else + // idx = pos - Base64; + // if (idx == 99) + // printf(" %2d, ", idx); + // else + // printf(" %2d/*%c*/,", idx, j); + // } + // printf("\n "); + // } + // } + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 62 /*-*/, 99, 99, + 52 /*0*/, 53 /*1*/, 54 /*2*/, 55 /*3*/, 56 /*4*/, 57 /*5*/, 58 /*6*/, 59 /*7*/, + 60 /*8*/, 61 /*9*/, 99, 99, 99, 99, 99, 99, + 99, 0 /*A*/, 1 /*B*/, 2 /*C*/, 3 /*D*/, 4 /*E*/, 5 /*F*/, 6 /*G*/, + 7 /*H*/, 8 /*I*/, 9 /*J*/, 10 /*K*/, 11 /*L*/, 12 /*M*/, 13 /*N*/, 14 /*O*/, + 15 /*P*/, 16 /*Q*/, 17 /*R*/, 18 /*S*/, 19 /*T*/, 20 /*U*/, 21 /*V*/, 22 /*W*/, + 23 /*X*/, 24 /*Y*/, 25 /*Z*/, 99, 99, 99, 99, 63 /*_*/, + 99, 26 /*a*/, 27 /*b*/, 28 /*c*/, 29 /*d*/, 30 /*e*/, 31 /*f*/, 32 /*g*/, + 33 /*h*/, 34 /*i*/, 35 /*j*/, 36 /*k*/, 37 /*l*/, 38 /*m*/, 39 /*n*/, 40 /*o*/, + 41 /*p*/, 42 /*q*/, 43 /*r*/, 44 /*s*/, 45 /*t*/, 46 /*u*/, 47 /*v*/, 48 /*w*/, + 49 /*x*/, 50 /*y*/, 51 /*z*/, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99}; + +// Tests a character to see if it's a whitespace character. +// +// Returns: +// YES if the character is a whitespace character. +// NO if the character is not a whitespace character. +// +BOOL QN_IsSpace(unsigned char c) { + // we use our own mapping here because we don't want anything w/ locale + // support. + static BOOL kSpaces[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // 0-9 + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 10-19 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20-29 + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, // 30-39 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40-49 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 50-59 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60-69 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 70-79 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80-89 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90-99 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 100-109 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 110-119 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 120-129 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 130-139 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 140-149 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 150-159 + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-169 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 170-179 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 180-189 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 190-199 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 200-209 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 210-219 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 220-229 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 230-239 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 240-249 + 0, 0, 0, 0, 0, 1, // 250-255 + }; + return kSpaces[c]; +} + +// Calculate how long the data will be once it's base64 encoded. +// +// Returns: +// The guessed encoded length for a source length +// +NSUInteger QN_CalcEncodedLength(NSUInteger srcLen, BOOL padded) { + NSUInteger intermediate_result = 8 * srcLen + 5; + NSUInteger len = intermediate_result / 6; + if (padded) { + len = ((len + 3) / 4) * 4; + } + return len; +} + +// Tries to calculate how long the data will be once it's base64 decoded. +// Unlike the above, this is always an upperbound, since the source data +// could have spaces and might end with the padding characters on them. +// +// Returns: +// The guessed decoded length for a source length +// +NSUInteger QN_GuessDecodedLength(NSUInteger srcLen) { + return (srcLen + 3) / 4 * 3; +} + +@interface QN_GTM_Base64 (PrivateMethods) + ++ (NSData *)baseEncode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + padded:(BOOL)padded; + ++ (NSData *)baseDecode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + requirePadding:(BOOL)requirePadding; + ++ (NSUInteger)baseEncode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + padded:(BOOL)padded; + ++ (NSUInteger)baseDecode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + requirePadding:(BOOL)requirePadding; + +@end + +@implementation QN_GTM_Base64 + +// +// Standard Base64 (RFC) handling +// + ++ (NSData *)encodeData:(NSData *)data { + return [self baseEncode:[data bytes] + length:[data length] + charset:kBase64EncodeChars + padded:YES]; +} + ++ (NSData *)decodeData:(NSData *)data { + return [self baseDecode:[data bytes] + length:[data length] + charset:kBase64DecodeChars + requirePadding:YES]; +} + ++ (NSData *)encodeBytes:(const void *)bytes length:(NSUInteger)length { + return [self baseEncode:bytes + length:length + charset:kBase64EncodeChars + padded:YES]; +} + ++ (NSData *)decodeBytes:(const void *)bytes length:(NSUInteger)length { + return [self baseDecode:bytes + length:length + charset:kBase64DecodeChars + requirePadding:YES]; +} + ++ (NSString *)stringByEncodingData:(NSData *)data { + NSString *result = nil; + NSData *converted = [self baseEncode:[data bytes] + length:[data length] + charset:kBase64EncodeChars + padded:YES]; + if (converted) { + result = [[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding]; + } + return result; +} + ++ (NSString *)stringByEncodingBytes:(const void *)bytes length:(NSUInteger)length { + NSString *result = nil; + NSData *converted = [self baseEncode:bytes + length:length + charset:kBase64EncodeChars + padded:YES]; + if (converted) { + result = [[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding]; + } + return result; +} + ++ (NSData *)decodeString:(NSString *)string { + NSData *result = nil; + NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding]; + if (data) { + result = [self baseDecode:[data bytes] + length:[data length] + charset:kBase64DecodeChars + requirePadding:YES]; + } + return result; +} + +// +// Modified Base64 encoding so the results can go onto urls. +// +// The changes are in the characters generated and also the result isn't +// padded to a multiple of 4. +// Must use the matching call to encode/decode, won't interop with the +// RFC versions. +// + ++ (NSData *)webSafeEncodeData:(NSData *)data + padded:(BOOL)padded { + return [self baseEncode:[data bytes] + length:[data length] + charset:kWebSafeBase64EncodeChars + padded:padded]; +} + ++ (NSData *)webSafeDecodeData:(NSData *)data { + return [self baseDecode:[data bytes] + length:[data length] + charset:kWebSafeBase64DecodeChars + requirePadding:NO]; +} + ++ (NSData *)webSafeEncodeBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded { + return [self baseEncode:bytes + length:length + charset:kWebSafeBase64EncodeChars + padded:padded]; +} + ++ (NSData *)webSafeDecodeBytes:(const void *)bytes length:(NSUInteger)length { + return [self baseDecode:bytes + length:length + charset:kWebSafeBase64DecodeChars + requirePadding:NO]; +} + ++ (NSString *)stringByWebSafeEncodingData:(NSData *)data + padded:(BOOL)padded { + NSString *result = nil; + NSData *converted = [self baseEncode:[data bytes] + length:[data length] + charset:kWebSafeBase64EncodeChars + padded:padded]; + if (converted) { + result = [[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding]; + } + return result; +} + ++ (NSString *)stringByWebSafeEncodingBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded { + NSString *result = nil; + NSData *converted = [self baseEncode:bytes + length:length + charset:kWebSafeBase64EncodeChars + padded:padded]; + if (converted) { + result = [[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding]; + } + return result; +} + ++ (NSData *)webSafeDecodeString:(NSString *)string { + NSData *result = nil; + NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding]; + if (data) { + result = [self baseDecode:[data bytes] + length:[data length] + charset:kWebSafeBase64DecodeChars + requirePadding:NO]; + } + return result; +} + +@end + +@implementation QN_GTM_Base64 (PrivateMethods) + +// +// baseEncode:length:charset:padded: +// +// Does the common lifting of creating the dest NSData. it creates & sizes the +// data for the results. |charset| is the characters to use for the encoding +// of the data. |padding| controls if the encoded data should be padded to a +// multiple of 4. +// +// Returns: +// an autorelease NSData with the encoded data, nil if any error. +// ++ (NSData *)baseEncode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + padded:(BOOL)padded { + // how big could it be? + NSUInteger maxLength = QN_CalcEncodedLength(length, padded); + // make space + NSMutableData *result = [NSMutableData data]; + [result setLength:maxLength]; + // do it + NSUInteger finalLength = [self baseEncode:bytes + srcLen:length + destBytes:[result mutableBytes] + destLen:[result length] + charset:charset + padded:padded]; + if (finalLength) { + // _GTMDevAssert(finalLength == maxLength, @"how did we calc the length wrong?"); + } else { + // shouldn't happen, this means we ran out of space + result = nil; + } + return result; +} + +// +// baseDecode:length:charset:requirePadding: +// +// Does the common lifting of creating the dest NSData. it creates & sizes the +// data for the results. |charset| is the characters to use for the decoding +// of the data. +// +// Returns: +// an autorelease NSData with the decoded data, nil if any error. +// +// ++ (NSData *)baseDecode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + requirePadding:(BOOL)requirePadding { + // could try to calculate what it will end up as + NSUInteger maxLength = QN_GuessDecodedLength(length); + // make space + NSMutableData *result = [NSMutableData data]; + [result setLength:maxLength]; + // do it + NSUInteger finalLength = [self baseDecode:bytes + srcLen:length + destBytes:[result mutableBytes] + destLen:[result length] + charset:charset + requirePadding:requirePadding]; + if (finalLength) { + if (finalLength != maxLength) { + // resize down to how big it was + [result setLength:finalLength]; + } + } else { + // either an error in the args, or we ran out of space + result = nil; + } + return result; +} + +// +// baseEncode:srcLen:destBytes:destLen:charset:padded: +// +// Encodes the buffer into the larger. returns the length of the encoded +// data, or zero for an error. +// |charset| is the characters to use for the encoding +// |padded| tells if the result should be padded to a multiple of 4. +// +// Returns: +// the length of the encoded data. zero if any error. +// ++ (NSUInteger)baseEncode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + padded:(BOOL)padded { + if (!srcLen || !destLen || !srcBytes || !destBytes) { + return 0; + } + + char *curDest = destBytes; + const unsigned char *curSrc = (const unsigned char *)(srcBytes); + + // Three bytes of data encodes to four characters of cyphertext. + // So we can pump through three-byte chunks atomically. + while (srcLen > 2) { + // space? + // _GTMDevAssert(destLen >= 4, @"our calc for encoded length was wrong"); + curDest[0] = charset[curSrc[0] >> 2]; + curDest[1] = charset[((curSrc[0] & 0x03) << 4) + (curSrc[1] >> 4)]; + curDest[2] = charset[((curSrc[1] & 0x0f) << 2) + (curSrc[2] >> 6)]; + curDest[3] = charset[curSrc[2] & 0x3f]; + + curDest += 4; + curSrc += 3; + srcLen -= 3; + destLen -= 4; + } + + // now deal with the tail (<=2 bytes) + switch (srcLen) { + case 0: + // Nothing left; nothing more to do. + break; + + case 1: + // One byte left: this encodes to two characters, and (optionally) + // two pad characters to round out the four-character cypherblock. + // _GTMDevAssert(destLen >= 2, @"our calc for encoded length was wrong"); + curDest[0] = charset[curSrc[0] >> 2]; + curDest[1] = charset[(curSrc[0] & 0x03) << 4]; + curDest += 2; + destLen -= 2; + if (padded) { + // _GTMDevAssert(destLen >= 2, @"our calc for encoded length was wrong"); + curDest[0] = kBase64PaddingChar; + curDest[1] = kBase64PaddingChar; + curDest += 2; + } + break; + + case 2: + // Two bytes left: this encodes to three characters, and (optionally) + // one pad character to round out the four-character cypherblock. + // _GTMDevAssert(destLen >= 3, @"our calc for encoded length was wrong"); + curDest[0] = charset[curSrc[0] >> 2]; + curDest[1] = charset[((curSrc[0] & 0x03) << 4) + (curSrc[1] >> 4)]; + curDest[2] = charset[(curSrc[1] & 0x0f) << 2]; + curDest += 3; + destLen -= 3; + if (padded) { + // _GTMDevAssert(destLen >= 1, @"our calc for encoded length was wrong"); + curDest[0] = kBase64PaddingChar; + curDest += 1; + } + break; + } + // return the length + return (curDest - destBytes); +} + +// +// baseDecode:srcLen:destBytes:destLen:charset:requirePadding: +// +// Decodes the buffer into the larger. returns the length of the decoded +// data, or zero for an error. +// |charset| is the character decoding buffer to use +// +// Returns: +// the length of the encoded data. zero if any error. +// ++ (NSUInteger)baseDecode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + requirePadding:(BOOL)requirePadding { + if (!srcLen || !destLen || !srcBytes || !destBytes) { + return 0; + } + + int decode; + NSUInteger destIndex = 0; + int state = 0; + char ch = 0; + while (srcLen-- && (ch = *srcBytes++) != 0) { + if (QN_IsSpace(ch)) // Skip whitespace + continue; + + if (ch == kBase64PaddingChar) + break; + + decode = charset[(unsigned int)ch]; + if (decode == kBase64InvalidChar) + return 0; + + // Four cyphertext characters decode to three bytes. + // Therefore we can be in one of four states. + switch (state) { + case 0: + // We're at the beginning of a four-character cyphertext block. + // This sets the high six bits of the first byte of the + // plaintext block. + // _GTMDevAssert(destIndex < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] = decode << 2; + state = 1; + break; + + case 1: + // We're one character into a four-character cyphertext block. + // This sets the low two bits of the first plaintext byte, + // and the high four bits of the second plaintext byte. + // _GTMDevAssert((destIndex+1) < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] |= decode >> 4; + destBytes[destIndex + 1] = (decode & 0x0f) << 4; + destIndex++; + state = 2; + break; + + case 2: + // We're two characters into a four-character cyphertext block. + // This sets the low four bits of the second plaintext + // byte, and the high two bits of the third plaintext byte. + // However, if this is the end of data, and those two + // bits are zero, it could be that those two bits are + // leftovers from the encoding of data that had a length + // of two mod three. + // _GTMDevAssert((destIndex+1) < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] |= decode >> 2; + destBytes[destIndex + 1] = (decode & 0x03) << 6; + destIndex++; + state = 3; + break; + + case 3: + // We're at the last character of a four-character cyphertext block. + // This sets the low six bits of the third plaintext byte. + // _GTMDevAssert(destIndex < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] |= decode; + destIndex++; + state = 0; + break; + } + } + + // We are done decoding Base-64 chars. Let's see if we ended + // on a byte boundary, and/or with erroneous trailing characters. + if (ch == kBase64PaddingChar) { // We got a pad char + if ((state == 0) || (state == 1)) { + return 0; // Invalid '=' in first or second position + } + if (srcLen == 0) { + if (state == 2) { // We run out of input but we still need another '=' + return 0; + } + // Otherwise, we are in state 3 and only need this '=' + } else { + if (state == 2) { // need another '=' + while ((ch = *srcBytes++) && (srcLen-- > 0)) { + if (!QN_IsSpace(ch)) + break; + } + if (ch != kBase64PaddingChar) { + return 0; + } + } + // state = 1 or 2, check if all remain padding is space + while ((ch = *srcBytes++) && (srcLen-- > 0)) { + if (!QN_IsSpace(ch)) { + return 0; + } + } + } + } else { + // We ended by seeing the end of the string. + + if (requirePadding) { + // If we require padding, then anything but state 0 is an error. + if (state != 0) { + return 0; + } + } else { + // Make sure we have no partial bytes lying around. Note that we do not + // require trailing '=', so states 2 and 3 are okay too. + if (state == 1) { + return 0; + } + } + } + + // If then next piece of output was valid and got written to it means we got a + // very carefully crafted input that appeared valid but contains some trailing + // bits past the real length, so just toss the thing. + if ((destIndex < destLen) && + (destBytes[destIndex] != 0)) { + return 0; + } + + return destIndex; +} + +@end diff --git a/msext/QiniuSDK/Http/QNHttpDelegate.h b/msext/QiniuSDK/Http/QNHttpDelegate.h new file mode 100755 index 0000000..b73d62e --- /dev/null +++ b/msext/QiniuSDK/Http/QNHttpDelegate.h @@ -0,0 +1,33 @@ +#import + +@class QNResponseInfo; + +typedef void (^QNInternalProgressBlock)(long long totalBytesWritten, long long totalBytesExpectedToWrite); +typedef void (^QNCompleteBlock)(QNResponseInfo *info, NSDictionary *resp); +typedef BOOL (^QNCancelBlock)(void); + +/** + * Http 客户端接口 + */ +@protocol QNHttpDelegate + +- (void)multipartPost:(NSString *)url + withData:(NSData *)data + withParams:(NSDictionary *)params + withFileName:(NSString *)key + withMimeType:(NSString *)mime + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access; + +- (void)post:(NSString *)url + withData:(NSData *)data + withParams:(NSDictionary *)params + withHeaders:(NSDictionary *)headers + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access; + +@end diff --git a/msext/QiniuSDK/Http/QNResponseInfo.h b/msext/QiniuSDK/Http/QNResponseInfo.h new file mode 100755 index 0000000..7978ba2 --- /dev/null +++ b/msext/QiniuSDK/Http/QNResponseInfo.h @@ -0,0 +1,206 @@ +// +// QNResponseInfo.h +// QiniuSDK +// +// Created by bailong on 14/10/2. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * 中途取消的状态码 + */ +extern const int kQNRequestCancelled; + +/** + * 网络错误状态码 + */ +extern const int kQNNetworkError; + +/** + * 错误参数状态码 + */ +extern const int kQNInvalidArgument; + +/** + * 0 字节文件或数据 + */ +extern const int kQNZeroDataSize; + +/** + * 错误token状态码 + */ +extern const int kQNInvalidToken; + +/** + * 读取文件错误状态码 + */ +extern const int kQNFileError; + +/** + * 上传完成后返回的状态信息 + */ +@interface QNResponseInfo : NSObject + +/** + * 状态码 + */ +@property (readonly) int statusCode; + +/** + * 七牛服务器生成的请求ID,用来跟踪请求信息,如果使用过程中出现问题,请反馈此ID + */ +@property (nonatomic, copy, readonly) NSString *reqId; + +/** + * 七牛服务器内部跟踪记录 + */ +@property (nonatomic, copy, readonly) NSString *xlog; + +/** + * cdn服务器内部跟踪记录 + */ +@property (nonatomic, copy, readonly) NSString *xvia; + +/** + * 错误信息,出错时请反馈此记录 + */ +@property (nonatomic, copy, readonly) NSError *error; + +/** + * 服务器域名 + */ +@property (nonatomic, copy, readonly) NSString *host; + +/** + * 请求消耗的时间,单位 秒 + */ +@property (nonatomic, readonly) double duration; + +/** + * 服务器IP + */ +@property (nonatomic, readonly) NSString *serverIp; + +/** + * 客户端id + */ +@property (nonatomic, readonly) NSString *id; + +/** + * 时间戳 + */ +@property (readonly) UInt64 timeStamp; + +/** + * 网络类型 + */ +//@property (nonatomic, readonly) NSString *networkType; + +/** + * 是否取消 + */ +@property (nonatomic, readonly, getter=isCancelled) BOOL canceled; + +/** + * 成功的请求 + */ +@property (nonatomic, readonly, getter=isOK) BOOL ok; + +/** + * 是否网络错误 + */ +@property (nonatomic, readonly, getter=isConnectionBroken) BOOL broken; + +/** + * 是否需要重试,内部使用 + */ +@property (nonatomic, readonly) BOOL couldRetry; + +/** + * 是否需要换备用server,内部使用 + */ +@property (nonatomic, readonly) BOOL needSwitchServer; + +/** + * 是否为 七牛响应 + */ +@property (nonatomic, readonly, getter=isNotQiniu) BOOL notQiniu; + +/** + * 工厂函数,内部使用 + * + * @return 取消的实例 + */ ++ (instancetype)cancel; + +/** + * 工厂函数,内部使用 + * + * @param desc 错误参数描述 + * + * @return 错误参数实例 + */ ++ (instancetype)responseInfoWithInvalidArgument:(NSString *)desc; + +/** + * 工厂函数,内部使用 + * + * @param desc 错误token描述 + * + * @return 错误token实例 + */ ++ (instancetype)responseInfoWithInvalidToken:(NSString *)desc; + +/** + * 工厂函数,内部使用 + * + * @param error 错误信息 + * @param host 服务器域名 + * @param duration 请求完成时间,单位秒 + * + * @return 网络错误实例 + */ ++ (instancetype)responseInfoWithNetError:(NSError *)error + host:(NSString *)host + duration:(double)duration; + +/** + * 工厂函数,内部使用 + * + * @param error 错误信息 + * + * @return 文件错误实例 + */ ++ (instancetype)responseInfoWithFileError:(NSError *)error; + +/** + * 工厂函数,内部使用 + * + * @return 文件错误实例 + */ ++ (instancetype)responseInfoOfZeroData:(NSString *)path; + +/** + * 构造函数 + * + * @param status 状态码 + * @param reqId 七牛服务器请求id + * @param xlog 七牛服务器记录 + * @param body 服务器返回内容 + * @param host 服务器域名 + * @param duration 请求完成时间,单位秒 + * + * @return 实例 + */ +- (instancetype)init:(int)status + withReqId:(NSString *)reqId + withXLog:(NSString *)xlog + withXVia:(NSString *)xvia + withHost:(NSString *)host + withIp:(NSString *)ip + withDuration:(double)duration + withBody:(NSData *)body; + +@end diff --git a/msext/QiniuSDK/Http/QNResponseInfo.m b/msext/QiniuSDK/Http/QNResponseInfo.m new file mode 100755 index 0000000..7dda06f --- /dev/null +++ b/msext/QiniuSDK/Http/QNResponseInfo.m @@ -0,0 +1,210 @@ +// +// QNResponseInfo.m +// QiniuSDK +// +// Created by bailong on 14/10/2. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNResponseInfo.h" +#import "QNUserAgent.h" +#import "QNVersion.h" + +const int kQNZeroDataSize = -6; +const int kQNInvalidToken = -5; +const int kQNFileError = -4; +const int kQNInvalidArgument = -3; +const int kQNRequestCancelled = -2; +const int kQNNetworkError = -1; + +/** + https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Constants/index.html#//apple_ref/doc/constant_group/URL_Loading_System_Error_Codes + + NSURLErrorUnknown = -1, + NSURLErrorCancelled = -999, + NSURLErrorBadURL = -1000, + NSURLErrorTimedOut = -1001, + NSURLErrorUnsupportedURL = -1002, + NSURLErrorCannotFindHost = -1003, + NSURLErrorCannotConnectToHost = -1004, + NSURLErrorDataLengthExceedsMaximum = -1103, + NSURLErrorNetworkConnectionLost = -1005, + NSURLErrorDNSLookupFailed = -1006, + NSURLErrorHTTPTooManyRedirects = -1007, + NSURLErrorResourceUnavailable = -1008, + NSURLErrorNotConnectedToInternet = -1009, + NSURLErrorRedirectToNonExistentLocation = -1010, + NSURLErrorBadServerResponse = -1011, + NSURLErrorUserCancelledAuthentication = -1012, + NSURLErrorUserAuthenticationRequired = -1013, + NSURLErrorZeroByteResource = -1014, + NSURLErrorCannotDecodeRawData = -1015, + NSURLErrorCannotDecodeContentData = -1016, + NSURLErrorCannotParseResponse = -1017, + NSURLErrorInternationalRoamingOff = -1018, + NSURLErrorCallIsActive = -1019, + NSURLErrorDataNotAllowed = -1020, + NSURLErrorRequestBodyStreamExhausted = -1021, + NSURLErrorFileDoesNotExist = -1100, + NSURLErrorFileIsDirectory = -1101, + NSURLErrorNoPermissionsToReadFile = -1102, + NSURLErrorSecureConnectionFailed = -1200, + NSURLErrorServerCertificateHasBadDate = -1201, + NSURLErrorServerCertificateUntrusted = -1202, + NSURLErrorServerCertificateHasUnknownRoot = -1203, + NSURLErrorServerCertificateNotYetValid = -1204, + NSURLErrorClientCertificateRejected = -1205, + NSURLErrorClientCertificateRequired = -1206, + NSURLErrorCannotLoadFromNetwork = -2000, + NSURLErrorCannotCreateFile = -3000, + NSURLErrorCannotOpenFile = -3001, + NSURLErrorCannotCloseFile = -3002, + NSURLErrorCannotWriteToFile = -3003, + NSURLErrorCannotRemoveFile = -3004, + NSURLErrorCannotMoveFile = -3005, + NSURLErrorDownloadDecodingFailedMidStream = -3006, + NSURLErrorDownloadDecodingFailedToComplete = -3007 + */ + +static QNResponseInfo *cancelledInfo = nil; + +static NSString *domain = @"qiniu.com"; + +@implementation QNResponseInfo + ++ (instancetype)cancel { + return [[QNResponseInfo alloc] initWithCancelled]; +} + ++ (instancetype)responseInfoWithInvalidArgument:(NSString *)text { + return [[QNResponseInfo alloc] initWithStatus:kQNInvalidArgument errorDescription:text]; +} + ++ (instancetype)responseInfoWithInvalidToken:(NSString *)text { + return [[QNResponseInfo alloc] initWithStatus:kQNInvalidToken errorDescription:text]; +} + ++ (instancetype)responseInfoWithNetError:(NSError *)error host:(NSString *)host duration:(double)duration { + int code = kQNNetworkError; + if (error != nil) { + code = (int)error.code; + } + return [[QNResponseInfo alloc] initWithStatus:code error:error host:host duration:duration]; +} + ++ (instancetype)responseInfoWithFileError:(NSError *)error { + return [[QNResponseInfo alloc] initWithStatus:kQNFileError error:error]; +} + ++ (instancetype)responseInfoOfZeroData:(NSString *)path { + NSString *desc; + if (path == nil) { + desc = @"data size is 0"; + } else { + desc = [[NSString alloc] initWithFormat:@"file %@ size is 0", path]; + } + return [[QNResponseInfo alloc] initWithStatus:kQNZeroDataSize errorDescription:desc]; +} + +- (instancetype)initWithCancelled { + return [self initWithStatus:kQNRequestCancelled errorDescription:@"cancelled by user"]; +} + +- (instancetype)initWithStatus:(int)status + error:(NSError *)error { + return [self initWithStatus:status error:error host:nil duration:0]; +} + +- (instancetype)initWithStatus:(int)status + error:(NSError *)error + host:(NSString *)host + duration:(double)duration { + if (self = [super init]) { + _statusCode = status; + _error = error; + _host = host; + _duration = duration; + _id = [QNUserAgent sharedInstance].id; + _timeStamp = [[NSDate date] timeIntervalSince1970]; + } + return self; +} + +- (instancetype)initWithStatus:(int)status + errorDescription:(NSString *)text { + NSError *error = [[NSError alloc] initWithDomain:domain code:status userInfo:@{ @"error" : text }]; + return [self initWithStatus:status error:error]; +} + +- (instancetype)init:(int)status + withReqId:(NSString *)reqId + withXLog:(NSString *)xlog + withXVia:(NSString *)xvia + withHost:(NSString *)host + withIp:(NSString *)ip + withDuration:(double)duration + withBody:(NSData *)body { + if (self = [super init]) { + _statusCode = status; + _reqId = [reqId copy]; + _xlog = [xlog copy]; + _xvia = [xvia copy]; + _host = [host copy]; + _duration = duration; + _serverIp = ip; + _id = [QNUserAgent sharedInstance].id; + _timeStamp = [[NSDate date] timeIntervalSince1970]; + if (status != 200) { + if (body == nil) { + _error = [[NSError alloc] initWithDomain:domain code:_statusCode userInfo:nil]; + } else { + NSError *tmp; + NSDictionary *uInfo = [NSJSONSerialization JSONObjectWithData:body options:NSJSONReadingMutableLeaves error:&tmp]; + if (tmp != nil) { + // 出现错误时,如果信息是非UTF8编码会失败,返回nil + NSString *str = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]; + if (str == nil) { + str = @""; + } + uInfo = @{ @"error" : str }; + } + _error = [[NSError alloc] initWithDomain:domain code:_statusCode userInfo:uInfo]; + } + } else if (body == nil || body.length == 0) { + NSDictionary *uInfo = @{ @"error" : @"no response json" }; + _error = [[NSError alloc] initWithDomain:domain code:_statusCode userInfo:uInfo]; + } + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@= id: %@, ver: %@, status: %d, requestId: %@, xlog: %@, xvia: %@, host: %@ ip: %@ duration: %f s time: %llu error: %@>", NSStringFromClass([self class]), _id, kQiniuVersion, _statusCode, _reqId, _xlog, _xvia, _host, _serverIp, _duration, _timeStamp, _error]; +} + +- (BOOL)isCancelled { + return _statusCode == kQNRequestCancelled || _statusCode == -999; +} + +- (BOOL)isNotQiniu { + return (_statusCode >= 200 && _statusCode < 500) && _reqId == nil; +} + +- (BOOL)isOK { + return _statusCode == 200 && _error == nil && _reqId != nil; +} + +- (BOOL)isConnectionBroken { + // reqId is nill means the server is not qiniu + return _statusCode == kQNNetworkError || (_statusCode < -1000 && _statusCode != -1003); +} + +- (BOOL)needSwitchServer { + return _statusCode == kQNNetworkError || (_statusCode < -1000 && _statusCode != -1003) || (_statusCode / 100 == 5 && _statusCode != 579); +} + +- (BOOL)couldRetry { + return (_statusCode >= 500 && _statusCode < 600 && _statusCode != 579) || _statusCode == kQNNetworkError || _statusCode == 996 || _statusCode == 406 || (_statusCode == 200 && _error != nil) || _statusCode < -1000 || self.isNotQiniu; +} + +@end diff --git a/msext/QiniuSDK/Http/QNSessionManager.h b/msext/QiniuSDK/Http/QNSessionManager.h new file mode 100755 index 0000000..7b915d7 --- /dev/null +++ b/msext/QiniuSDK/Http/QNSessionManager.h @@ -0,0 +1,40 @@ +#import "QNHttpDelegate.h" +#import + +#import "QNConfiguration.h" + +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090) + +@interface QNSessionManager : NSObject + +- (instancetype)initWithProxy:(NSDictionary *)proxyDict + timeout:(UInt32)timeout + urlConverter:(QNUrlConvert)converter + dns:(QNDnsManager *)dns; + +- (void)multipartPost:(NSString *)url + withData:(NSData *)data + withParams:(NSDictionary *)params + withFileName:(NSString *)key + withMimeType:(NSString *)mime + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access; + +- (void)post:(NSString *)url + withData:(NSData *)data + withParams:(NSDictionary *)params + withHeaders:(NSDictionary *)headers + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access; + +- (void)get:(NSString *)url + withHeaders:(NSDictionary *)headers + withCompleteBlock:(QNCompleteBlock)completeBlock; + +@end + +#endif diff --git a/msext/QiniuSDK/Http/QNSessionManager.m b/msext/QiniuSDK/Http/QNSessionManager.m new file mode 100755 index 0000000..a369d9a --- /dev/null +++ b/msext/QiniuSDK/Http/QNSessionManager.m @@ -0,0 +1,333 @@ +// +// QNHttpManager.m +// QiniuSDK +// +// Created by bailong on 14/10/1. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "AFNetworking.h" + +#import "HappyDNS.h" +#import "QNAsyncRun.h" +#import "QNConfiguration.h" +#import "QNResponseInfo.h" +#import "QNSessionManager.h" +#include "QNSystem.h" +#import "QNUserAgent.h" + +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090) + +@interface QNProgessDelegate : NSObject +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context; +@property (nonatomic, strong) QNInternalProgressBlock progressBlock; +@property (nonatomic, strong) NSProgress *progress; +@property (nonatomic, strong) NSURLSessionUploadTask *task; +@property (nonatomic, strong) QNCancelBlock cancelBlock; +- (instancetype)initWithProgress:(QNInternalProgressBlock)progressBlock; +@end + +static NSURL *buildUrl(NSString *host, NSNumber *port, NSString *path) { + port = port == nil ? [NSNumber numberWithInt:80] : port; + NSString *p = [[NSString alloc] initWithFormat:@"http://%@:%@%@", host, port, path]; + return [[NSURL alloc] initWithString:p]; +} + +static BOOL needRetry(NSHTTPURLResponse *httpResponse, NSError *error) { + if (error != nil) { + return error.code < -1000; + } + if (httpResponse == nil) { + return YES; + } + int status = (int)httpResponse.statusCode; + return status >= 500 && status < 600 && status != 579; +} + +@implementation QNProgessDelegate +- (instancetype)initWithProgress:(QNInternalProgressBlock)progressBlock { + if (self = [super init]) { + _progressBlock = progressBlock; + _progress = nil; + } + + return self; +} + +- (void)valueChange:(NSProgress *)uploadProgress { + _progressBlock(uploadProgress.completedUnitCount, uploadProgress.totalUnitCount); + if (_cancelBlock && _cancelBlock()) { + [_task cancel]; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context; +{ + if (context == nil || object == nil) { + return; + } + + NSProgress *progress = (NSProgress *)object; + + void *p = (__bridge void *)(self); + if (p == context) { + _progressBlock(progress.completedUnitCount, progress.totalUnitCount); + if (_cancelBlock && _cancelBlock()) { + [_task cancel]; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +@end + +@interface QNSessionManager () +@property (nonatomic) AFHTTPSessionManager *httpManager; +@property UInt32 timeout; +@property (nonatomic, strong) QNUrlConvert converter; +@property bool noProxy; +@property (nonatomic) QNDnsManager *dns; +@end + +@implementation QNSessionManager + +- (instancetype)initWithProxy:(NSDictionary *)proxyDict + timeout:(UInt32)timeout + urlConverter:(QNUrlConvert)converter + dns:(QNDnsManager *)dns { + if (self = [super init]) { + if (proxyDict != nil) { + _noProxy = NO; + } else { + _noProxy = YES; + } + + _httpManager = [QNSessionManager httpManagerWithProxy:proxyDict]; + + _timeout = timeout; + _converter = converter; + _dns = dns; + } + + return self; +} + ++ (AFHTTPSessionManager *)httpManagerWithProxy:(NSDictionary *)proxyDict { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + if (proxyDict != nil) { + configuration.connectionProxyDictionary = proxyDict; + } + + AFHTTPSessionManager *httpManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration]; + httpManager.responseSerializer = [AFHTTPResponseSerializer serializer]; + return httpManager; +} + +- (instancetype)init { + return [self initWithProxy:nil timeout:60 urlConverter:nil dns:nil]; +} + ++ (QNResponseInfo *)buildResponseInfo:(NSHTTPURLResponse *)response + withError:(NSError *)error + withDuration:(double)duration + withResponse:(NSData *)body + withHost:(NSString *)host + withIp:(NSString *)ip { + QNResponseInfo *info; + + if (response) { + int status = (int)[response statusCode]; + NSDictionary *headers = [response allHeaderFields]; + NSString *reqId = headers[@"X-Reqid"]; + NSString *xlog = headers[@"X-Log"]; + NSString *xvia = headers[@"X-Via"]; + if (xvia == nil) { + xvia = headers[@"X-Px"]; + } + if (xvia == nil) { + xvia = headers[@"Fw-Via"]; + } + info = [[QNResponseInfo alloc] init:status withReqId:reqId withXLog:xlog withXVia:xvia withHost:host withIp:ip withDuration:duration withBody:body]; + } else { + info = [QNResponseInfo responseInfoWithNetError:error host:host duration:duration]; + } + return info; +} + +- (void)sendRequest:(NSMutableURLRequest *)request + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access { + __block NSDate *startTime = [NSDate date]; + NSString *domain = request.URL.host; + NSString *u = request.URL.absoluteString; + NSURL *url = request.URL; + NSArray *ips = nil; + if (_converter != nil) { + url = [[NSURL alloc] initWithString:_converter(u)]; + request.URL = url; + domain = url.host; + } else if (_noProxy && _dns != nil && [url.scheme isEqualToString:@"http"]) { + if (isIpV6FullySupported() || ![QNIP isV6]) { + ips = [_dns queryWithDomain:[[QNDomain alloc] init:domain hostsFirst:NO hasCname:YES maxTtl:1000]]; + double duration = [[NSDate date] timeIntervalSinceDate:startTime]; + + if (ips == nil || ips.count == 0) { + NSError *error = [[NSError alloc] initWithDomain:domain code:-1003 userInfo:@{ @"error" : @"unkonwn host" }]; + + QNResponseInfo *info = [QNResponseInfo responseInfoWithNetError:error host:domain duration:duration]; + NSLog(@"failure %@", info); + + completeBlock(info, nil); + return; + } + } + } + [self sendRequest2:request withCompleteBlock:completeBlock withProgressBlock:progressBlock withCancelBlock:cancelBlock withIpArray:ips withIndex:0 withDomain:domain withRetryTimes:3 withStartTime:startTime withAccess:access]; +} + +- (void)sendRequest2:(NSMutableURLRequest *)request + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withIpArray:(NSArray *)ips + withIndex:(int)index + withDomain:(NSString *)domain + withRetryTimes:(int)times + withStartTime:(NSDate *)startTime + withAccess:(NSString *)access { + NSURL *url = request.URL; + __block NSString *ip = nil; + if (ips != nil) { + ip = [ips objectAtIndex:(index % ips.count)]; + NSString *path = url.path; + if (path == nil || [@"" isEqualToString:path]) { + path = @"/"; + } + url = buildUrl(ip, url.port, path); + [request setValue:domain forHTTPHeaderField:@"Host"]; + } + request.URL = url; + [request setTimeoutInterval:_timeout]; + [request setValue:[[QNUserAgent sharedInstance] getUserAgent:access] forHTTPHeaderField:@"User-Agent"]; + [request setValue:nil forHTTPHeaderField:@"Accept-Language"]; + if (progressBlock == nil) { + progressBlock = ^(long long totalBytesWritten, long long totalBytesExpectedToWrite) { + }; + } + QNInternalProgressBlock progressBlock2 = ^(long long totalBytesWritten, long long totalBytesExpectedToWrite) { + progressBlock(totalBytesWritten, totalBytesExpectedToWrite); + }; + __block QNProgessDelegate *delegate = [[QNProgessDelegate alloc] initWithProgress:progressBlock2]; + + NSURLSessionUploadTask *uploadTask = [_httpManager uploadTaskWithRequest:request fromData:nil progress:^(NSProgress *_Nonnull uploadProgress) { + [delegate valueChange:uploadProgress]; + } + completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + NSData *data = responseObject; + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + double duration = [[NSDate date] timeIntervalSinceDate:startTime]; + QNResponseInfo *info; + NSDictionary *resp = nil; + if (_converter != nil && _noProxy && (index + 1 < ips.count || times > 0) && needRetry(httpResponse, error)) { + [self sendRequest2:request withCompleteBlock:completeBlock withProgressBlock:progressBlock withCancelBlock:cancelBlock withIpArray:ips withIndex:index + 1 withDomain:domain withRetryTimes:times - 1 withStartTime:startTime withAccess:access]; + return; + } + if (error == nil) { + info = [QNSessionManager buildResponseInfo:httpResponse withError:nil withDuration:duration withResponse:data withHost:domain withIp:ip]; + if (info.isOK) { + NSError *tmp; + resp = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&tmp]; + } + } else { + info = [QNSessionManager buildResponseInfo:httpResponse withError:error withDuration:duration withResponse:data withHost:domain withIp:ip]; + } + completeBlock(info, resp); + }]; + delegate.task = uploadTask; + delegate.cancelBlock = cancelBlock; + + [uploadTask resume]; +} + +- (void)multipartPost:(NSString *)url + withData:(NSData *)data + withParams:(NSDictionary *)params + withFileName:(NSString *)key + withMimeType:(NSString *)mime + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access { + NSMutableURLRequest *request = [_httpManager.requestSerializer multipartFormRequestWithMethod:@"POST" + URLString:url + parameters:params + constructingBodyWithBlock:^(id formData) { + [formData appendPartWithFileData:data name:@"file" fileName:key mimeType:mime]; + } + error:nil]; + [self sendRequest:request withCompleteBlock:completeBlock withProgressBlock:progressBlock withCancelBlock:cancelBlock + withAccess:access]; +} + +- (void)post:(NSString *)url + withData:(NSData *)data + withParams:(NSDictionary *)params + withHeaders:(NSDictionary *)headers + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock + withCancelBlock:(QNCancelBlock)cancelBlock + withAccess:(NSString *)access { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:url]]; + if (headers) { + [request setAllHTTPHeaderFields:headers]; + } + [request setHTTPMethod:@"POST"]; + if (params) { + [request setValuesForKeysWithDictionary:params]; + } + [request setHTTPBody:data]; + QNAsyncRun(^{ + [self sendRequest:request + withCompleteBlock:completeBlock + withProgressBlock:progressBlock + withCancelBlock:cancelBlock + withAccess:access]; + }); +} + +- (void)get:(NSString *)url + withHeaders:(NSDictionary *)headers + withCompleteBlock:(QNCompleteBlock)completeBlock { + QNAsyncRun(^{ + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; + + NSURL *URL = [NSURL URLWithString:url]; + NSURLRequest *request = [NSURLRequest requestWithURL:URL]; + + NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + NSData *s = [@"{}" dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *resp = nil; + QNResponseInfo *info; + if (error == nil) { + info = [QNSessionManager buildResponseInfo:httpResponse withError:nil withDuration:0 withResponse:s withHost:@"" withIp:@""]; + if (info.isOK) { + resp = responseObject; + } + } else { + info = [QNSessionManager buildResponseInfo:httpResponse withError:error withDuration:0 withResponse:s withHost:@"" withIp:@""]; + } + + completeBlock(info, resp); + }]; + [dataTask resume]; + }); +} + +@end + +#endif diff --git a/msext/QiniuSDK/Http/QNUserAgent.h b/msext/QiniuSDK/Http/QNUserAgent.h new file mode 100755 index 0000000..be92fb3 --- /dev/null +++ b/msext/QiniuSDK/Http/QNUserAgent.h @@ -0,0 +1,37 @@ +// +// QNUserAgent.h +// QiniuSDK +// +// Created by bailong on 14-9-29. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * UserAgent + * + */ + +@interface QNUserAgent : NSObject + +/** + * 用户id + */ +@property (copy, nonatomic, readonly) NSString *id; + +/** + * UserAgent 字串 + */ +- (NSString *)description; + +/** + * UserAgent + AK 字串 + */ +- (NSString *)getUserAgent:(NSString *)access; + +/** + * 单例 + */ ++ (instancetype)sharedInstance; +@end diff --git a/msext/QiniuSDK/Http/QNUserAgent.m b/msext/QiniuSDK/Http/QNUserAgent.m new file mode 100755 index 0000000..dc45597 --- /dev/null +++ b/msext/QiniuSDK/Http/QNUserAgent.m @@ -0,0 +1,86 @@ +// +// QNUserAgent.m +// QiniuSDK +// +// Created by bailong on 14-9-29. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import +#if __IPHONE_OS_VERSION_MIN_REQUIRED +#import +#import +#else +#import +#endif + +#import "QNUserAgent.h" +#import "QNVersion.h" + +static NSString *qn_clientId(void) { +#if __IPHONE_OS_VERSION_MIN_REQUIRED + NSString *s = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + if (s == nil) { + s = @"simulator"; + } + return s; +#else + long long now_timestamp = [[NSDate date] timeIntervalSince1970] * 1000; + int r = arc4random() % 1000; + return [NSString stringWithFormat:@"%lld%u", now_timestamp, r]; +#endif +} + +static NSString *qn_userAgent(NSString *id, NSString *ak) { +#if __IPHONE_OS_VERSION_MIN_REQUIRED + return [NSString stringWithFormat:@"QiniuObject-C/%@ (%@; iOS %@; %@; %@)", kQiniuVersion, [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], id, ak]; +#else + return [NSString stringWithFormat:@"QiniuObject-C/%@ (Mac OS X %@; %@; %@)", kQiniuVersion, [[NSProcessInfo processInfo] operatingSystemVersionString], id, ak]; +#endif +} + +@interface QNUserAgent () +@property (nonatomic) NSString *ua; +@end + +@implementation QNUserAgent + +- (NSString *)description { + return _ua; +} + +- (instancetype)init { + if (self = [super init]) { + _id = qn_clientId(); + } + return self; +} + +/** + * UserAgent + */ +- (NSString *)getUserAgent:(NSString *)access { + NSString *ak; + if (access == nil || access.length == 0) { + ak = @"-"; + } else { + ak = access; + } + return qn_userAgent(_id, ak); +} + +/** + * 单例 + */ ++ (instancetype)sharedInstance { + static QNUserAgent *sharedInstance = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + + return sharedInstance; +} + +@end diff --git a/msext/QiniuSDK/QiniuSDK.h b/msext/QiniuSDK/QiniuSDK.h new file mode 100755 index 0000000..d9f2e6d --- /dev/null +++ b/msext/QiniuSDK/QiniuSDK.h @@ -0,0 +1,16 @@ +// +// QiniuSDK.h +// QiniuSDK +// +// Created by bailong on 14-9-28. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +#import "QNConfiguration.h" +#import "QNFileRecorder.h" +#import "QNResponseInfo.h" +#import "QNUploadManager.h" +#import "QNUploadOption.h" +#import "QNUrlSafeBase64.h" diff --git a/msext/QiniuSDK/Recorder/QNFileRecorder.h b/msext/QiniuSDK/Recorder/QNFileRecorder.h new file mode 100755 index 0000000..30bae0e --- /dev/null +++ b/msext/QiniuSDK/Recorder/QNFileRecorder.h @@ -0,0 +1,52 @@ +// +// QNFileRecorder.h +// QiniuSDK +// +// Created by bailong on 14/10/5. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNRecorderDelegate.h" +#import + +/** + * 将上传记录保存到文件系统中 + */ +@interface QNFileRecorder : NSObject + +/** + * 用指定保存的目录进行初始化 + * + * @param directory 目录 + * @param error 输出的错误信息 + * + * @return 实例 + */ ++ (instancetype)fileRecorderWithFolder:(NSString *)directory + error:(NSError *__autoreleasing *)error; + +/** + * 用指定保存的目录,以及是否进行base64编码进行初始化, + * + * @param directory 目录 + * @param encode 为避免因为特殊字符或含有/,无法保存持久化记录,故用此参数指定是否要base64编码 + * @param error 输出错误信息 + * + * @return 实例 + */ ++ (instancetype)fileRecorderWithFolder:(NSString *)directory + encodeKey:(BOOL)encode + error:(NSError *__autoreleasing *)error; + +/** + * 从外部手动删除记录,如无特殊需求,不建议使用 + * + * @param key 持久化记录key + * @param dir 目录 + * @param encode 是否encode + */ ++ (void)removeKey:(NSString *)key + directory:(NSString *)dir + encodeKey:(BOOL)encode; + +@end diff --git a/msext/QiniuSDK/Recorder/QNFileRecorder.m b/msext/QiniuSDK/Recorder/QNFileRecorder.m new file mode 100755 index 0000000..09ffb67 --- /dev/null +++ b/msext/QiniuSDK/Recorder/QNFileRecorder.m @@ -0,0 +1,99 @@ +// +// QNFileRecorder.m +// QiniuSDK +// +// Created by bailong on 14/10/5. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNFileRecorder.h" +#import "QNUrlSafeBase64.h" + +@interface QNFileRecorder () + +@property (copy, readonly) NSString *directory; +@property BOOL encode; + +@end + +@implementation QNFileRecorder + +- (NSString *)pathOfKey:(NSString *)key { + return [QNFileRecorder pathJoin:key path:_directory]; +} + ++ (NSString *)pathJoin:(NSString *)key + path:(NSString *)path { + return [[NSString alloc] initWithFormat:@"%@/%@", path, key]; +} + ++ (instancetype)fileRecorderWithFolder:(NSString *)directory + error:(NSError *__autoreleasing *)perror { + return [QNFileRecorder fileRecorderWithFolder:directory encodeKey:false error:perror]; +} + ++ (instancetype)fileRecorderWithFolder:(NSString *)directory + encodeKey:(BOOL)encode + error:(NSError *__autoreleasing *)perror { + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]; + if (error != nil) { + if (perror) { + *perror = error; + } + return nil; + } + + return [[QNFileRecorder alloc] initWithFolder:directory encodeKey:encode]; +} + +- (instancetype)initWithFolder:(NSString *)directory encodeKey:(BOOL)encode { + if (self = [super init]) { + _directory = directory; + _encode = encode; + } + return self; +} + +- (NSError *)set:(NSString *)key + data:(NSData *)value { + NSError *error; + if (_encode) { + key = [QNUrlSafeBase64 encodeString:key]; + } + [value writeToFile:[self pathOfKey:key] options:NSDataWritingAtomic error:&error]; + return error; +} + +- (NSData *)get:(NSString *)key { + if (_encode) { + key = [QNUrlSafeBase64 encodeString:key]; + } + return [NSData dataWithContentsOfFile:[self pathOfKey:key]]; +} + +- (NSError *)del:(NSString *)key { + NSError *error; + if (_encode) { + key = [QNUrlSafeBase64 encodeString:key]; + } + [[NSFileManager defaultManager] removeItemAtPath:[self pathOfKey:key] error:&error]; + return error; +} + ++ (void)removeKey:(NSString *)key + directory:(NSString *)dir + encodeKey:(BOOL)encode { + if (encode) { + key = [QNUrlSafeBase64 encodeString:key]; + } + NSError *error; + NSString *path = [QNFileRecorder pathJoin:key path:dir]; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, dir: %@>", NSStringFromClass([self class]), self, _directory]; +} + +@end diff --git a/msext/QiniuSDK/Recorder/QNRecorderDelegate.h b/msext/QiniuSDK/Recorder/QNRecorderDelegate.h new file mode 100755 index 0000000..b308730 --- /dev/null +++ b/msext/QiniuSDK/Recorder/QNRecorderDelegate.h @@ -0,0 +1,55 @@ +// +// QNRecorderDelegate.h +// QiniuSDK +// +// Created by bailong on 14/10/5. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * 为持久化上传记录,根据上传的key以及文件名 生成持久化的记录key + * + * @param uploadKey 上传的key + * @param filePath 文件名 + * + * @return 根据uploadKey, filepath 算出的记录key + */ +typedef NSString * (^QNRecorderKeyGenerator)(NSString *uploadKey, NSString *filePath); + +/** + * 持久化记录接口,可以实现将记录持久化到文件,数据库等 + */ +@protocol QNRecorderDelegate + +/** + * 保存记录 + * + * @param key 持久化记录的key + * @param value 持久化记录上传状态信息 + * + * @return 错误信息,成功为nil + */ +- (NSError *)set:(NSString *)key + data:(NSData *)value; + +/** + * 取出保存的持久化上传状态信息 + * + * @param key 持久化记录key + * + * @return 上传中间状态信息 + */ +- (NSData *)get:(NSString *)key; + +/** + * 删除持久化记录,一般在上传成功后自动调用 + * + * @param key 持久化记录key + * + * @return 错误信息,成功为nil + */ +- (NSError *)del:(NSString *)key; + +@end diff --git a/msext/QiniuSDK/Storage/QNConfiguration.h b/msext/QiniuSDK/Storage/QNConfiguration.h new file mode 100755 index 0000000..37946a4 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNConfiguration.h @@ -0,0 +1,205 @@ +// +// QNConfiguration.h +// QiniuSDK +// +// Created by bailong on 15/5/21. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import + +#import "QNRecorderDelegate.h" + +/** + * 断点上传时的分块大小 + */ +extern const UInt32 kQNBlockSize; + +/** + * 转换为用户需要的url + * + * @param url 上传url + * + * @return 根据上传url算出代理url + */ +typedef NSString * (^QNUrlConvert)(NSString *url); + +@class QNConfigurationBuilder; +@class QNDnsManager; +@class QNServiceAddress; +@class QNZone; +/** + * Builder block + * + * @param builder builder实例 + */ +typedef void (^QNConfigurationBuilderBlock)(QNConfigurationBuilder *builder); + +@interface QNConfiguration : NSObject + +/** + * 存储区域 + */ +@property (copy, nonatomic, readonly) QNZone *zone; + +/** + * 断点上传时的分片大小 + */ +@property (readonly) UInt32 chunkSize; + +/** + * 如果大于此值就使用断点上传,否则使用form上传 + */ +@property (readonly) UInt32 putThreshold; + +/** + * 上传失败的重试次数 + */ +@property (readonly) UInt32 retryMax; + +/** + * 超时时间 单位 秒 + */ +@property (readonly) UInt32 timeoutInterval; + +@property (nonatomic, readonly) id recorder; + +@property (nonatomic, readonly) QNRecorderKeyGenerator recorderKeyGen; + +@property (nonatomic, readonly) NSDictionary *proxy; + +@property (nonatomic, readonly) QNUrlConvert converter; + +@property (nonatomic, readonly) QNDnsManager *dns; + +@property (readonly) BOOL disableATS; + ++ (instancetype)build:(QNConfigurationBuilderBlock)block; + +@end + +/** + * 上传服务地址 + */ +@interface QNServiceAddress : NSObject + +- (instancetype)init:(NSString *)address ips:(NSArray *)ips; + +@property (nonatomic, readonly) NSString *address; +@property (nonatomic, readonly) NSArray *ips; + +@end + +typedef void (^QNPrequeryReturn)(int code); + +@class QNUpToken; + +@interface QNZone : NSObject + +/** + * 默认上传服务器地址 + */ +- (QNServiceAddress *)up:(QNUpToken *)token; + +/** + * 备用上传服务器地址 + */ +- (QNServiceAddress *)upBackup:(QNUpToken *)token; + +/** + * zone 0 华东 + * + * @return 实例 + */ ++ (instancetype)zone0; + +/** + * zone 1 华北 + * + * @return 实例 + */ ++ (instancetype)zone1; + +/** + * zone 2 华南 + * + * @return 实例 + */ ++ (instancetype)zone2; + +/** + * zone Na0 北美 + * + * @return 实例 + */ ++ (instancetype)zoneNa0; + +- (void)preQuery:(QNUpToken *)token + on:(QNPrequeryReturn)ret; + ++ (void)addIpToDns:(QNDnsManager *)dns; + +@end + +@interface QNFixedZone : QNZone +/** + * Zone初始化方法 + * + * @param upHost 默认上传服务器地址 + * @param upHostBackup 备用上传服务器地址 + * @param upIp 备用上传IP + * + * @return Zone实例 + */ +- (instancetype)initWithUp:(QNServiceAddress *)up + upBackup:(QNServiceAddress *)upBackup; + +@end + +@interface QNAutoZone : QNZone + +- (instancetype)initWithHttps:(BOOL)flag + dns:(QNDnsManager *)dns; + +@end + +@interface QNConfigurationBuilder : NSObject + +/** + * 默认上传服务器地址 + */ +@property (nonatomic, strong) QNZone *zone; + +/** + * 断点上传时的分片大小 + */ +@property (assign) UInt32 chunkSize; + +/** + * 如果大于此值就使用断点上传,否则使用form上传 + */ +@property (assign) UInt32 putThreshold; + +/** + * 上传失败的重试次数 + */ +@property (assign) UInt32 retryMax; + +/** + * 超时时间 单位 秒 + */ +@property (assign) UInt32 timeoutInterval; + +@property (nonatomic, strong) id recorder; + +@property (nonatomic, strong) QNRecorderKeyGenerator recorderKeyGen; + +@property (nonatomic, strong) NSDictionary *proxy; + +@property (nonatomic, strong) QNUrlConvert converter; + +@property (nonatomic, strong) QNDnsManager *dns; + +@property (assign) BOOL disableATS; + +@end diff --git a/msext/QiniuSDK/Storage/QNConfiguration.m b/msext/QiniuSDK/Storage/QNConfiguration.m new file mode 100755 index 0000000..6c84fa0 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNConfiguration.m @@ -0,0 +1,360 @@ +// +// QNConfiguration.m +// QiniuSDK +// +// Created by bailong on 15/5/21. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNConfiguration.h" +#import "HappyDNS.h" +#import "QNNetworkInfo.h" +#import "QNResponseInfo.h" +#import "QNSessionManager.h" +#import "QNSystem.h" +#import "QNUpToken.h" + +const UInt32 kQNBlockSize = 4 * 1024 * 1024; + +static void addServiceToDns(QNServiceAddress *address, QNDnsManager *dns) { + NSArray *ips = address.ips; + if (ips == nil) { + return; + } + NSURL *u = [[NSURL alloc] initWithString:address.address]; + NSString *host = u.host; + for (int i = 0; i < ips.count; i++) { + [dns putHosts:host ip:ips[i]]; + } +} + +static void addZoneToDns(QNZone *zone, QNDnsManager *dns) { + addServiceToDns([zone up:nil], dns); + addServiceToDns([zone upBackup:nil], dns); +} + +static QNDnsManager *initDns(QNConfigurationBuilder *builder) { + QNDnsManager *d = builder.dns; + if (d == nil) { + id r1 = [QNResolver systemResolver]; + id r2 = [[QNResolver alloc] initWithAddress:@"119.29.29.29"]; + id r3 = [[QNResolver alloc] initWithAddress:@"114.114.115.115"]; + d = [[QNDnsManager alloc] init:[NSArray arrayWithObjects:r1, r2, r3, nil] networkInfo:[QNNetworkInfo normal]]; + } + return d; +} + +@implementation QNConfiguration + ++ (instancetype)build:(QNConfigurationBuilderBlock)block { + QNConfigurationBuilder *builder = [[QNConfigurationBuilder alloc] init]; + block(builder); + return [[QNConfiguration alloc] initWithBuilder:builder]; +} + +- (instancetype)initWithBuilder:(QNConfigurationBuilder *)builder { + if (self = [super init]) { + + _chunkSize = builder.chunkSize; + _putThreshold = builder.putThreshold; + _retryMax = builder.retryMax; + _timeoutInterval = builder.timeoutInterval; + + _recorder = builder.recorder; + _recorderKeyGen = builder.recorderKeyGen; + + _proxy = builder.proxy; + + _converter = builder.converter; + + _disableATS = builder.disableATS; + if (_disableATS) { + _dns = initDns(builder); + [QNZone addIpToDns:_dns]; + } else { + _dns = nil; + } + _zone = builder.zone; + } + return self; +} + +@end + +@implementation QNConfigurationBuilder + +- (instancetype)init { + if (self = [super init]) { + _zone = [QNZone zone0]; + _chunkSize = 256 * 1024; + _putThreshold = 512 * 1024; + _retryMax = 2; + _timeoutInterval = 60; + + _recorder = nil; + _recorderKeyGen = nil; + + _proxy = nil; + _converter = nil; + + if (hasAts() && !allowsArbitraryLoads()) { + _disableATS = NO; + } else { + _disableATS = YES; + } + } + return self; +} + +@end + +@implementation QNServiceAddress : NSObject + +- (instancetype)init:(NSString *)address ips:(NSArray *)ips { + if (self = [super init]) { + _address = address; + _ips = ips; + } + return self; +} + +@end + +@implementation QNFixedZone { + QNServiceAddress *up; + QNServiceAddress *upBackup; +} + +/** + * 备用上传服务器地址 + */ +- (QNServiceAddress *)upBackup:(NSString *)token { + return upBackup; +} + +- (QNServiceAddress *)up:(NSString *)token { + return up; +} + +- (instancetype)initWithUp:(QNServiceAddress *)up1 + upBackup:(QNServiceAddress *)upBackup1 { + if (self = [super init]) { + up = up1; + upBackup = upBackup1; + } + + return self; +} +@end + +@interface QNAutoZoneInfo : NSObject +@property (readonly, nonatomic) NSString *upHost; +@property (readonly, nonatomic) NSString *upIp; +@property (readonly, nonatomic) NSString *upBackup; +@property (readonly, nonatomic) NSString *upHttps; + +- (instancetype)init:(NSString *)uphost + upIp:(NSString *)upip + upBackup:(NSString *)upbackup + upHttps:(NSString *)uphttps; +@end + +@implementation QNAutoZoneInfo + +- (instancetype)init:(NSString *)uphost + upIp:(NSString *)upip + upBackup:(NSString *)upbackup + upHttps:(NSString *)uphttps { + if (self = [super init]) { + _upHost = uphost; + _upIp = upip; + _upBackup = upbackup; + _upHttps = uphttps; + } + return self; +} + +@end + +@implementation QNAutoZone { + NSString *server; + BOOL https; + NSMutableDictionary *cache; + NSLock *lock; + QNSessionManager *sesionManager; + QNDnsManager *dns; +} + +- (instancetype)initWithHttps:(BOOL)flag + dns:(QNDnsManager *)dns1 { + if (self = [super init]) { + dns = dns1; + server = @"https://uc.qbox.me"; + https = flag; + cache = [NSMutableDictionary new]; + lock = [NSLock new]; + sesionManager = [[QNSessionManager alloc] initWithProxy:nil timeout:10 urlConverter:nil dns:dns]; + } + return self; +} + +- (QNServiceAddress *)upBackup:(QNUpToken *)token { + NSString *index = [token index]; + [lock lock]; + QNAutoZoneInfo *info = [cache objectForKey:index]; + [lock unlock]; + if (info == nil) { + return nil; + } + if (https) { + return [[QNServiceAddress alloc] init:info.upHttps ips:@[ info.upIp ]]; + } + return [[QNServiceAddress alloc] init:info.upBackup ips:@[ info.upIp ]]; +} + +- (QNServiceAddress *)up:(QNUpToken *)token { + NSString *index = [token index]; + [lock lock]; + QNAutoZoneInfo *info = [cache objectForKey:index]; + [lock unlock]; + if (info == nil) { + return nil; + } + if (https) { + return [[QNServiceAddress alloc] init:info.upHttps ips:@[ info.upIp ]]; + } + return [[QNServiceAddress alloc] init:info.upHost ips:@[ info.upIp ]]; +} + +- (QNAutoZoneInfo *)buildInfoFromJson:(NSDictionary *)resp { + NSDictionary *http = [resp objectForKey:@"http"]; + NSArray *up = [http objectForKey:@"up"]; + NSString *upHost = [up objectAtIndex:1]; + NSString *upBackup = [up objectAtIndex:0]; + NSString *ipTemp = [up objectAtIndex:2]; + NSArray *a1 = [ipTemp componentsSeparatedByString:@" "]; + NSString *ip1 = [a1 objectAtIndex:2]; + NSArray *a2 = [ip1 componentsSeparatedByString:@"//"]; + NSString *upIp = [a2 objectAtIndex:1]; + NSDictionary *https_ = [resp objectForKey:@"https"]; + NSArray *a3 = [https_ objectForKey:@"up"]; + NSString *upHttps = [a3 objectAtIndex:0]; + return [[QNAutoZoneInfo alloc] init:upHost upIp:upIp upBackup:upBackup upHttps:upHttps]; +} + +- (void)preQuery:(QNUpToken *)token + on:(QNPrequeryReturn)ret { + if (token == nil) { + ret(-1); + } + [lock lock]; + QNAutoZoneInfo *info = [cache objectForKey:[token index]]; + [lock unlock]; + if (info != nil) { + ret(0); + return; + } + + NSString *url = [NSString stringWithFormat:@"%@/v1/query?ak=%@&bucket=%@", server, token.access, token.bucket]; + [sesionManager get:url withHeaders:nil withCompleteBlock:^(QNResponseInfo *info, NSDictionary *resp) { + if ([info isOK]) { + QNAutoZoneInfo *info = [self buildInfoFromJson:resp]; + if (info == nil) { + ret(kQNInvalidToken); + } else { + ret(0); + [lock lock]; + [cache setValue:info forKey:[token index]]; + [lock unlock]; + if (dns != nil) { + QNServiceAddress *address = [[QNServiceAddress alloc] init:info.upHttps ips:@[ info.upIp ]]; + addServiceToDns(address, dns); + address = [[QNServiceAddress alloc] init:info.upHost ips:@[ info.upIp ]]; + addServiceToDns(address, dns); + address = [[QNServiceAddress alloc] init:info.upBackup ips:@[ info.upIp ]]; + addServiceToDns(address, dns); + } + } + } else { + ret(kQNNetworkError); + } + }]; +} + +@end + +@implementation QNZone + +- (instancetype)init { + self = [super init]; + return self; +} + +/** + * 备用上传服务器地址 + */ +- (QNServiceAddress *)upBackup:(QNUpToken *)token { + return nil; +} + +- (QNServiceAddress *)up:(QNUpToken *)token { + return nil; +} + ++ (instancetype)createWithHost:(NSString *)up backupHost:(NSString *)backup ip1:(NSString *)ip1 ip2:(NSString *)ip2 { + NSArray *ips = [NSArray arrayWithObjects:ip1, ip2, nil]; + NSString *a = [NSString stringWithFormat:@"http://%@", up]; + QNServiceAddress *s1 = [[QNServiceAddress alloc] init:a ips:ips]; + NSString *b = [NSString stringWithFormat:@"http://%@", backup]; + QNServiceAddress *s2 = [[QNServiceAddress alloc] init:b ips:ips]; + return [[QNFixedZone alloc] initWithUp:s1 upBackup:s2]; +} + ++ (instancetype)zone0 { + static QNZone *z0 = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + z0 = [QNZone createWithHost:@"upload.qiniu.com" backupHost:@"up.qiniu.com" ip1:@"183.136.139.10" ip2:@"115.231.182.136"]; + }); + return z0; +} + ++ (instancetype)zone1 { + static QNZone *z1 = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + z1 = [QNZone createWithHost:@"upload-z1.qiniu.com" backupHost:@"up-z1.qiniu.com" ip1:@"106.38.227.28" ip2:@"106.38.227.27"]; + }); + return z1; +} + ++ (instancetype)zone2 { + static QNZone *z2 = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + z2 = [QNZone createWithHost:@"upload-z2.qiniu.com" backupHost:@"up-z2.qiniu.com" ip1:@"14.152.37.7" ip2:@"183.60.214.199"]; + }); + return z2; +} + ++ (instancetype)zoneNa0 { + static QNZone *zNa0 = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + zNa0 = [QNZone createWithHost:@"upload-na0.qiniu.com" backupHost:@"up-na0.qiniu.com" ip1:@"14.152.37.7" ip2:@"183.60.214.199"]; + }); + return zNa0; +} + ++ (void)addIpToDns:(QNDnsManager *)dns { + addZoneToDns([QNZone zone0], dns); + addZoneToDns([QNZone zone1], dns); + addZoneToDns([QNZone zone2], dns); +} + +- (void)preQuery:(QNUpToken *)token + on:(QNPrequeryReturn)ret { + ret(0); +} + +@end diff --git a/msext/QiniuSDK/Storage/QNFormUpload.h b/msext/QiniuSDK/Storage/QNFormUpload.h new file mode 100755 index 0000000..179fc2f --- /dev/null +++ b/msext/QiniuSDK/Storage/QNFormUpload.h @@ -0,0 +1,26 @@ +// +// QNFormUpload.h +// QiniuSDK +// +// Created by bailong on 15/1/4. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNHttpDelegate.h" +#import "QNUpToken.h" +#import "QNUploadManager.h" +#import + +@interface QNFormUpload : NSObject + +- (instancetype)initWithData:(NSData *)data + withKey:(NSString *)key + withToken:(QNUpToken *)token + withCompletionHandler:(QNUpCompletionHandler)block + withOption:(QNUploadOption *)option + withHttpManager:(id)http + withConfiguration:(QNConfiguration *)config; + +- (void)put; + +@end diff --git a/msext/QiniuSDK/Storage/QNFormUpload.m b/msext/QiniuSDK/Storage/QNFormUpload.m new file mode 100755 index 0000000..30401ef --- /dev/null +++ b/msext/QiniuSDK/Storage/QNFormUpload.m @@ -0,0 +1,133 @@ +// +// QNFormUpload.m +// QiniuSDK +// +// Created by bailong on 15/1/4. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNFormUpload.h" +#import "QNConfiguration.h" +#import "QNCrc32.h" +#import "QNRecorderDelegate.h" +#import "QNResponseInfo.h" +#import "QNUploadManager.h" +#import "QNUploadOption+Private.h" +#import "QNUrlSafeBase64.h" + +@interface QNFormUpload () + +@property (nonatomic, strong) NSData *data; +@property (nonatomic, strong) id httpManager; +@property (nonatomic) int retryTimes; +@property (nonatomic, strong) NSString *key; +@property (nonatomic, strong) QNUpToken *token; +@property (nonatomic, strong) QNUploadOption *option; +@property (nonatomic, strong) QNUpCompletionHandler complete; +@property (nonatomic, strong) QNConfiguration *config; +@property (nonatomic) float previousPercent; + +@property (nonatomic, strong) NSString *access; //AK + +@end + +@implementation QNFormUpload + +- (instancetype)initWithData:(NSData *)data + withKey:(NSString *)key + withToken:(QNUpToken *)token + withCompletionHandler:(QNUpCompletionHandler)block + withOption:(QNUploadOption *)option + withHttpManager:(id)http + withConfiguration:(QNConfiguration *)config { + if (self = [super init]) { + _data = data; + _key = key; + _token = token; + _option = option != nil ? option : [QNUploadOption defaultOptions]; + _complete = block; + _httpManager = http; + _config = config; + _previousPercent = 0; + _access = token.access; + } + return self; +} + +- (void)put { + NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; + NSString *fileName = _key; + if (_key) { + parameters[@"key"] = _key; + } else { + fileName = @"?"; + } + + parameters[@"token"] = _token.token; + + [parameters addEntriesFromDictionary:_option.params]; + + if (_option.checkCrc) { + parameters[@"crc32"] = [NSString stringWithFormat:@"%u", (unsigned int)[QNCrc32 data:_data]]; + } + + QNInternalProgressBlock p = ^(long long totalBytesWritten, long long totalBytesExpectedToWrite) { + float percent = (float)totalBytesWritten / (float)totalBytesExpectedToWrite; + if (percent > 0.95) { + percent = 0.95; + } + if (percent > _previousPercent) { + _previousPercent = percent; + } else { + percent = _previousPercent; + } + _option.progressHandler(_key, percent); + }; + + QNCompleteBlock complete = ^(QNResponseInfo *info, NSDictionary *resp) { + if (info.isOK) { + _option.progressHandler(_key, 1.0); + } + if (info.isOK || !info.couldRetry) { + _complete(info, _key, resp); + return; + } + if (_option.cancellationSignal()) { + _complete([QNResponseInfo cancel], _key, nil); + return; + } + NSString *nextHost = [_config.zone up:_token].address; + if (info.isConnectionBroken || info.needSwitchServer) { + nextHost = [_config.zone upBackup:_token].address; + } + + QNCompleteBlock retriedComplete = ^(QNResponseInfo *info, NSDictionary *resp) { + if (info.isOK) { + _option.progressHandler(_key, 1.0); + } + _complete(info, _key, resp); + }; + + [_httpManager multipartPost:nextHost + withData:_data + withParams:parameters + withFileName:fileName + withMimeType:_option.mimeType + withCompleteBlock:retriedComplete + withProgressBlock:p + withCancelBlock:_option.cancellationSignal + withAccess:_access]; + }; + + [_httpManager multipartPost:[_config.zone up:_token].address + withData:_data + withParams:parameters + withFileName:fileName + withMimeType:_option.mimeType + withCompleteBlock:complete + withProgressBlock:p + withCancelBlock:_option.cancellationSignal + withAccess:_access]; +} + +@end diff --git a/msext/QiniuSDK/Storage/QNResumeUpload.h b/msext/QiniuSDK/Storage/QNResumeUpload.h new file mode 100755 index 0000000..c96fe7f --- /dev/null +++ b/msext/QiniuSDK/Storage/QNResumeUpload.h @@ -0,0 +1,29 @@ +// +// QNResumeUpload.h +// QiniuSDK +// +// Created by bailong on 14/10/1. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNFileDelegate.h" +#import "QNHttpDelegate.h" +#import "QNUpToken.h" +#import "QNUploadManager.h" +#import + +@interface QNResumeUpload : NSObject + +- (instancetype)initWithFile:(id)file + withKey:(NSString *)key + withToken:(QNUpToken *)token + withCompletionHandler:(QNUpCompletionHandler)block + withOption:(QNUploadOption *)option + withRecorder:(id)recorder + withRecorderKey:(NSString *)recorderKey + withHttpManager:(id)http + withConfiguration:(QNConfiguration *)config; + +- (void)run; + +@end diff --git a/msext/QiniuSDK/Storage/QNResumeUpload.m b/msext/QiniuSDK/Storage/QNResumeUpload.m new file mode 100755 index 0000000..1f6e799 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNResumeUpload.m @@ -0,0 +1,337 @@ +// +// QNResumeUpload.m +// QiniuSDK +// +// Created by bailong on 14/10/1. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNResumeUpload.h" +#import "QNConfiguration.h" +#import "QNCrc32.h" +#import "QNRecorderDelegate.h" +#import "QNResponseInfo.h" +#import "QNUploadManager.h" +#import "QNUploadOption+Private.h" +#import "QNUrlSafeBase64.h" + +typedef void (^task)(void); + +@interface QNResumeUpload () + +@property (nonatomic, strong) id httpManager; +@property UInt32 size; +@property (nonatomic) int retryTimes; +@property (nonatomic, strong) NSString *key; +@property (nonatomic, strong) NSString *recorderKey; +@property (nonatomic) NSDictionary *headers; +@property (nonatomic, strong) QNUploadOption *option; +@property (nonatomic, strong) QNUpToken *token; +@property (nonatomic, strong) QNUpCompletionHandler complete; +@property (nonatomic, strong) NSMutableArray *contexts; + +@property int64_t modifyTime; +@property (nonatomic, strong) id recorder; + +@property (nonatomic, strong) QNConfiguration *config; + +@property UInt32 chunkCrc; + +@property (nonatomic, strong) id file; + +//@property (nonatomic, strong) NSArray *fileAry; + +@property (nonatomic) float previousPercent; + +@property (nonatomic, strong) NSString *access; //AK + +- (void)makeBlock:(NSString *)uphost + offset:(UInt32)offset + blockSize:(UInt32)blockSize + chunkSize:(UInt32)chunkSize + progress:(QNInternalProgressBlock)progressBlock + complete:(QNCompleteBlock)complete; + +- (void)putChunk:(NSString *)uphost + offset:(UInt32)offset + size:(UInt32)size + context:(NSString *)context + progress:(QNInternalProgressBlock)progressBlock + complete:(QNCompleteBlock)complete; + +- (void)makeFile:(NSString *)uphost + complete:(QNCompleteBlock)complete; + +@end + +@implementation QNResumeUpload + +- (instancetype)initWithFile:(id)file + withKey:(NSString *)key + withToken:(QNUpToken *)token + withCompletionHandler:(QNUpCompletionHandler)block + withOption:(QNUploadOption *)option + withRecorder:(id)recorder + withRecorderKey:(NSString *)recorderKey + withHttpManager:(id)http + withConfiguration:(QNConfiguration *)config; +{ + if (self = [super init]) { + _file = file; + _size = (UInt32)[file size]; + _key = key; + NSString *tokenUp = [NSString stringWithFormat:@"UpToken %@", token.token]; + _option = option != nil ? option : [QNUploadOption defaultOptions]; + _complete = block; + _headers = @{@"Authorization" : tokenUp, @"Content-Type" : @"application/octet-stream"}; + _recorder = recorder; + _httpManager = http; + _modifyTime = [file modifyTime]; + _recorderKey = recorderKey; + _contexts = [[NSMutableArray alloc] initWithCapacity:(_size + kQNBlockSize - 1) / kQNBlockSize]; + _config = config; + + _token = token; + _previousPercent = 0; + + _access = token.access; + } + return self; +} + +// save json value +//{ +// "size":filesize, +// "offset":lastSuccessOffset, +// "modify_time": lastFileModifyTime, +// "contexts": contexts +//} + +- (void)record:(UInt32)offset { + NSString *key = self.recorderKey; + if (offset == 0 || _recorder == nil || key == nil || [key isEqualToString:@""]) { + return; + } + NSNumber *n_size = @(self.size); + NSNumber *n_offset = @(offset); + NSNumber *n_time = [NSNumber numberWithLongLong:_modifyTime]; + NSMutableDictionary *rec = [NSMutableDictionary dictionaryWithObjectsAndKeys:n_size, @"size", n_offset, @"offset", n_time, @"modify_time", _contexts, @"contexts", nil]; + + NSError *error; + NSData *data = [NSJSONSerialization dataWithJSONObject:rec options:NSJSONWritingPrettyPrinted error:&error]; + if (error != nil) { + NSLog(@"up record json error %@ %@", key, error); + return; + } + error = [_recorder set:key data:data]; + if (error != nil) { + NSLog(@"up record set error %@ %@", key, error); + } +} + +- (void)removeRecord { + if (_recorder == nil) { + return; + } + [_recorder del:self.recorderKey]; +} + +- (UInt32)recoveryFromRecord { + NSString *key = self.recorderKey; + if (_recorder == nil || key == nil || [key isEqualToString:@""]) { + return 0; + } + + NSData *data = [_recorder get:key]; + if (data == nil) { + return 0; + } + + NSError *error; + NSDictionary *info = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&error]; + if (error != nil) { + NSLog(@"recovery error %@ %@", key, error); + [_recorder del:self.key]; + return 0; + } + NSNumber *n_offset = info[@"offset"]; + NSNumber *n_size = info[@"size"]; + NSNumber *time = info[@"modify_time"]; + NSArray *contexts = info[@"contexts"]; + if (n_offset == nil || n_size == nil || time == nil || contexts == nil) { + return 0; + } + + UInt32 offset = [n_offset unsignedIntValue]; + UInt32 size = [n_size unsignedIntValue]; + if (offset > size || size != self.size) { + return 0; + } + UInt64 t = [time unsignedLongLongValue]; + if (t != _modifyTime) { + NSLog(@"modify time changed %llu, %llu", t, _modifyTime); + return 0; + } + _contexts = [[NSMutableArray alloc] initWithArray:contexts copyItems:true]; + return offset; +} + +- (void)nextTask:(UInt32)offset retriedTimes:(int)retried host:(NSString *)host { + if (self.option.cancellationSignal()) { + self.complete([QNResponseInfo cancel], self.key, nil); + return; + } + + if (offset == self.size) { + QNCompleteBlock completionHandler = ^(QNResponseInfo *info, NSDictionary *resp) { + if (info.isOK) { + [self removeRecord]; + self.option.progressHandler(self.key, 1.0); + } else if (info.couldRetry && retried < _config.retryMax) { + [self nextTask:offset retriedTimes:retried + 1 host:host]; + return; + } + self.complete(info, self.key, resp); + }; + [self makeFile:host complete:completionHandler]; + return; + } + + UInt32 chunkSize = [self calcPutSize:offset]; + QNInternalProgressBlock progressBlock = ^(long long totalBytesWritten, long long totalBytesExpectedToWrite) { + float percent = (float)(offset + totalBytesWritten) / (float)self.size; + if (percent > 0.95) { + percent = 0.95; + } + if (percent > _previousPercent) { + _previousPercent = percent; + } else { + percent = _previousPercent; + } + self.option.progressHandler(self.key, percent); + }; + + QNCompleteBlock completionHandler = ^(QNResponseInfo *info, NSDictionary *resp) { + if (info.error != nil) { + if (info.statusCode == 701) { + [self nextTask:(offset / kQNBlockSize) * kQNBlockSize retriedTimes:0 host:host]; + return; + } + if (retried >= _config.retryMax || !info.couldRetry) { + self.complete(info, self.key, resp); + return; + } + + NSString *nextHost = host; + if (info.isConnectionBroken || info.needSwitchServer) { + nextHost = [_config.zone upBackup:_token].address; + } + + [self nextTask:offset retriedTimes:retried + 1 host:nextHost]; + return; + } + + if (resp == nil) { + [self nextTask:offset retriedTimes:retried host:host]; + return; + } + + NSString *ctx = resp[@"ctx"]; + NSNumber *crc = resp[@"crc32"]; + if (ctx == nil || crc == nil || [crc unsignedLongValue] != _chunkCrc) { + [self nextTask:offset retriedTimes:retried host:host]; + return; + } + _contexts[offset / kQNBlockSize] = ctx; + [self record:offset + chunkSize]; + [self nextTask:offset + chunkSize retriedTimes:retried host:host]; + }; + if (offset % kQNBlockSize == 0) { + UInt32 blockSize = [self calcBlockSize:offset]; + [self makeBlock:host offset:offset blockSize:blockSize chunkSize:chunkSize progress:progressBlock complete:completionHandler]; + return; + } + NSString *context = _contexts[offset / kQNBlockSize]; + [self putChunk:host offset:offset size:chunkSize context:context progress:progressBlock complete:completionHandler]; +} + +- (UInt32)calcPutSize:(UInt32)offset { + UInt32 left = self.size - offset; + return left < _config.chunkSize ? left : _config.chunkSize; +} + +- (UInt32)calcBlockSize:(UInt32)offset { + UInt32 left = self.size - offset; + return left < kQNBlockSize ? left : kQNBlockSize; +} + +- (void)makeBlock:(NSString *)uphost + offset:(UInt32)offset + blockSize:(UInt32)blockSize + chunkSize:(UInt32)chunkSize + progress:(QNInternalProgressBlock)progressBlock + complete:(QNCompleteBlock)complete { + NSData *data = [self.file read:offset size:chunkSize]; + NSString *url = [[NSString alloc] initWithFormat:@"%@/mkblk/%u", uphost, (unsigned int)blockSize]; + _chunkCrc = [QNCrc32 data:data]; + [self post:url withData:data withCompleteBlock:complete withProgressBlock:progressBlock]; +} + +- (void)putChunk:(NSString *)uphost + offset:(UInt32)offset + size:(UInt32)size + context:(NSString *)context + progress:(QNInternalProgressBlock)progressBlock + complete:(QNCompleteBlock)complete { + NSData *data = [self.file read:offset size:size]; + UInt32 chunkOffset = offset % kQNBlockSize; + NSString *url = [[NSString alloc] initWithFormat:@"%@/bput/%@/%u", uphost, context, (unsigned int)chunkOffset]; + _chunkCrc = [QNCrc32 data:data]; + [self post:url withData:data withCompleteBlock:complete withProgressBlock:progressBlock]; +} + +- (void)makeFile:(NSString *)uphost + complete:(QNCompleteBlock)complete { + NSString *mime = [[NSString alloc] initWithFormat:@"/mimeType/%@", [QNUrlSafeBase64 encodeString:self.option.mimeType]]; + + __block NSString *url = [[NSString alloc] initWithFormat:@"%@/mkfile/%u%@", uphost, (unsigned int)self.size, mime]; + + if (self.key != nil) { + NSString *keyStr = [[NSString alloc] initWithFormat:@"/key/%@", [QNUrlSafeBase64 encodeString:self.key]]; + url = [NSString stringWithFormat:@"%@%@", url, keyStr]; + } + + [self.option.params enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { + url = [NSString stringWithFormat:@"%@/%@/%@", url, key, [QNUrlSafeBase64 encodeString:obj]]; + }]; + + //添加路径 + NSString *fname = [[NSString alloc] initWithFormat:@"/fname/%@", [QNUrlSafeBase64 encodeString:[self fileBaseName]]]; + url = [NSString stringWithFormat:@"%@%@", url, fname]; + + NSMutableData *postData = [NSMutableData data]; + NSString *bodyStr = [self.contexts componentsJoinedByString:@","]; + [postData appendData:[bodyStr dataUsingEncoding:NSUTF8StringEncoding]]; + [self post:url withData:postData withCompleteBlock:complete withProgressBlock:nil]; +} + +#pragma mark - 处理文件路径 +- (NSString *)fileBaseName { + return [[_file path] lastPathComponent]; +} + +- (void)post:(NSString *)url + withData:(NSData *)data + withCompleteBlock:(QNCompleteBlock)completeBlock + withProgressBlock:(QNInternalProgressBlock)progressBlock { + [_httpManager post:url withData:data withParams:nil withHeaders:_headers withCompleteBlock:completeBlock withProgressBlock:progressBlock withCancelBlock:_option.cancellationSignal withAccess:_access]; +} + +- (void)run { + @autoreleasepool { + UInt32 offset = [self recoveryFromRecord]; + [self nextTask:offset retriedTimes:0 host:[_config.zone up:_token].address]; + } +} + +@end diff --git a/msext/QiniuSDK/Storage/QNUpToken.h b/msext/QiniuSDK/Storage/QNUpToken.h new file mode 100755 index 0000000..03d3a4e --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUpToken.h @@ -0,0 +1,23 @@ +// +// QNUpToken.h +// QiniuSDK +// +// Created by bailong on 15/6/7. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import + +@interface QNUpToken : NSObject + ++ (instancetype)parse:(NSString *)token; + +@property (copy, nonatomic, readonly) NSString *access; +@property (copy, nonatomic, readonly) NSString *bucket; +@property (copy, nonatomic, readonly) NSString *token; + +@property (readonly) BOOL hasReturnUrl; + +- (NSString *)index; + +@end diff --git a/msext/QiniuSDK/Storage/QNUpToken.m b/msext/QiniuSDK/Storage/QNUpToken.m new file mode 100755 index 0000000..2473151 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUpToken.m @@ -0,0 +1,73 @@ +// +// QNUpToken.m +// QiniuSDK +// +// Created by bailong on 15/6/7. +// Copyright (c) 2015年 Qiniu. All rights reserved. +// + +#import "QNUrlSafeBase64.h" +#import "QNUpToken.h" + +@interface QNUpToken () + +- (instancetype)init:(NSDictionary *)policy token:(NSString *)token; + +@end + +@implementation QNUpToken + +- (instancetype)init:(NSDictionary *)policy token:(NSString *)token { + if (self = [super init]) { + _token = token; + _access = [self getAccess]; + _bucket = [self getBucket:policy]; + _hasReturnUrl = (policy[@"returnUrl"] != nil); + } + + return self; +} + +- (NSString *)getAccess { + + NSRange range = [_token rangeOfString:@":" options:NSCaseInsensitiveSearch]; + return [_token substringToIndex:range.location]; +} + +- (NSString *)getBucket:(NSDictionary *)info { + + NSString *scope = [info objectForKey:@"scope"]; + if (!scope) { + return @""; + } + + NSRange range = [scope rangeOfString:@":"]; + if (range.location == NSNotFound) { + return scope; + } + return [scope substringToIndex:range.location]; +} + ++ (instancetype)parse:(NSString *)token { + if (token == nil) { + return nil; + } + NSArray *array = [token componentsSeparatedByString:@":"]; + if (array == nil || array.count != 3) { + return nil; + } + + NSData *data = [QNUrlSafeBase64 decodeString:array[2]]; + NSError *tmp = nil; + NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&tmp]; + if (tmp != nil || dict[@"scope"] == nil || dict[@"deadline"] == nil) { + return nil; + } + return [[QNUpToken alloc] init:dict token:token]; +} + +- (NSString *)index { + return [NSString stringWithFormat:@"%@:%@", _access, _bucket]; +} + +@end diff --git a/msext/QiniuSDK/Storage/QNUploadManager.h b/msext/QiniuSDK/Storage/QNUploadManager.h new file mode 100755 index 0000000..43cda53 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUploadManager.h @@ -0,0 +1,155 @@ +// +// QNUploader.h +// QiniuSDK +// +// Created by bailong on 14-9-28. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +#import "QNRecorderDelegate.h" + +@class QNResponseInfo; +@class QNUploadOption; +@class QNConfiguration; +@class ALAsset; +@class PHAsset; +@class PHAssetResource; + +/** + * 上传完成后的回调函数 + * + * @param info 上下文信息,包括状态码,错误值 + * @param key 上传时指定的key,原样返回 + * @param resp 上传成功会返回文件信息,失败为nil; 可以通过此值是否为nil 判断上传结果 + */ +typedef void (^QNUpCompletionHandler)(QNResponseInfo *info, NSString *key, NSDictionary *resp); + +/** + 管理上传的类,可以生成一次,持续使用,不必反复创建。 + */ +@interface QNUploadManager : NSObject + +/** + * 默认构造方法,没有持久化记录 + * + * @return 上传管理类实例 + */ +- (instancetype)init; + +/** + * 使用一个持久化的记录接口进行记录的构造方法 + * + * @param recorder 持久化记录接口实现 + * + * @return 上传管理类实例 + */ +- (instancetype)initWithRecorder:(id)recorder; + +/** + * 使用持久化记录接口以及持久化key生成函数的构造方法,默认情况下使用上传存储的key, 如果key为nil或者有特殊字符比如/,建议使用自己的生成函数 + * + * @param recorder 持久化记录接口实现 + * @param recorderKeyGenerator 持久化记录key生成函数 + * + * @return 上传管理类实例 + */ +- (instancetype)initWithRecorder:(id)recorder + recorderKeyGenerator:(QNRecorderKeyGenerator)recorderKeyGenerator; + +/** + * 使用配置信息生成上传实例 + * + * @param config 配置信息 + * + * @return 上传管理类实例 + */ +- (instancetype)initWithConfiguration:(QNConfiguration *)config; + +/** + * 方便使用的单例方法 + * + * @param config 配置信息 + * + * @return 上传管理类实例 + */ ++ (instancetype)sharedInstanceWithConfiguration:(QNConfiguration *)config; + +/** + * 直接上传数据 + * + * @param data 待上传的数据 + * @param key 上传到云存储的key,为nil时表示是由七牛生成 + * @param token 上传需要的token, 由服务器生成 + * @param completionHandler 上传完成后的回调函数 + * @param option 上传时传入的可选参数 + */ +- (void)putData:(NSData *)data + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option; + +/** + * 上传文件 + * + * @param filePath 文件路径 + * @param key 上传到云存储的key,为nil时表示是由七牛生成 + * @param token 上传需要的token, 由服务器生成 + * @param completionHandler 上传完成后的回调函数 + * @param option 上传时传入的可选参数 + */ +- (void)putFile:(NSString *)filePath + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option; + +/** + * 上传ALAsset文件 + * + * @param alasset ALAsset文件 + * @param key 上传到云存储的key,为nil时表示是由七牛生成 + * @param token 上传需要的token, 由服务器生成 + * @param completionHandler 上传完成后的回调函数 + * @param option 上传时传入的可选参数 + */ +- (void)putALAsset:(ALAsset *)asset + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option; + +/** + * 上传PHAsset文件(IOS8 andLater) + * + * @param asset PHAsset文件 + * @param key 上传到云存储的key,为nil时表示是由七牛生成 + * @param token 上传需要的token, 由服务器生成 + * @param completionHandler 上传完成后的回调函数 + * @param option 上传时传入的可选参数 + */ +- (void)putPHAsset:(PHAsset *)asset + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option; + +/** + * 上传PHAssetResource文件(IOS9.1 andLater) + * + * @param asset PHAssetResource文件 + * @param key 上传到云存储的key,为nil时表示是由七牛生成 + * @param token 上传需要的token, 由服务器生成 + * @param completionHandler 上传完成后的回调函数 + * @param option 上传时传入的可选参数 + */ + +- (void)putPHAssetResource:(PHAssetResource *)assetResource + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option; + +@end diff --git a/msext/QiniuSDK/Storage/QNUploadManager.m b/msext/QiniuSDK/Storage/QNUploadManager.m new file mode 100755 index 0000000..789ddc3 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUploadManager.m @@ -0,0 +1,321 @@ +// +// QNUploader.h +// QiniuSDK +// +// Created by bailong on 14-9-28. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +#if __IPHONE_OS_VERSION_MIN_REQUIRED +#import "QNALAssetFile.h" +#import +#import +#import + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +#import "QNPHAssetFile.h" +#import +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90100 +#import "QNPHAssetResource.h" +#endif + +#else +#import +#endif + +#import "QNAsyncRun.h" +#import "QNConfiguration.h" +#import "QNCrc32.h" +#import "QNFile.h" +#import "QNFormUpload.h" +#import "QNResponseInfo.h" +#import "QNResumeUpload.h" +#import "QNSessionManager.h" +#import "QNSystem.h" +#import "QNUpToken.h" +#import "QNUploadManager.h" +#import "QNUploadOption+Private.h" + +@interface QNUploadManager () +@property (nonatomic) id httpManager; +@property (nonatomic) QNConfiguration *config; +@end + +@implementation QNUploadManager + +- (instancetype)init { + return [self initWithConfiguration:nil]; +} + +- (instancetype)initWithRecorder:(id)recorder { + return [self initWithRecorder:recorder recorderKeyGenerator:nil]; +} + +- (instancetype)initWithRecorder:(id)recorder + recorderKeyGenerator:(QNRecorderKeyGenerator)recorderKeyGenerator { + QNConfiguration *config = [QNConfiguration build:^(QNConfigurationBuilder *builder) { + builder.recorder = recorder; + builder.recorderKeyGen = recorderKeyGenerator; + }]; + return [self initWithConfiguration:config]; +} + +- (instancetype)initWithConfiguration:(QNConfiguration *)config { + if (self = [super init]) { + if (config == nil) { + config = [QNConfiguration build:^(QNConfigurationBuilder *builder){ + }]; + } + _config = config; +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090) + _httpManager = [[QNSessionManager alloc] initWithProxy:config.proxy timeout:config.timeoutInterval urlConverter:config.converter dns:config.dns]; +#endif + } + return self; +} + ++ (instancetype)sharedInstanceWithConfiguration:(QNConfiguration *)config { + static QNUploadManager *sharedInstance = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initWithConfiguration:config]; + }); + + return sharedInstance; +} + ++ (BOOL)checkAndNotifyError:(NSString *)key + token:(NSString *)token + input:(NSObject *)input + complete:(QNUpCompletionHandler)completionHandler { + NSString *desc = nil; + if (completionHandler == nil) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:@"no completionHandler" + userInfo:nil]; + return YES; + } + if (input == nil) { + desc = @"no input data"; + } else if (token == nil || [token isEqualToString:@""]) { + desc = @"no token"; + } + if (desc != nil) { + QNAsyncRunInMain(^{ + completionHandler([QNResponseInfo responseInfoWithInvalidArgument:desc], key, nil); + }); + return YES; + } + return NO; +} + +- (void)putData:(NSData *)data + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option { + if ([QNUploadManager checkAndNotifyError:key token:token input:data complete:completionHandler]) { + return; + } + + QNUpToken *t = [QNUpToken parse:token]; + if (t == nil) { + QNAsyncRunInMain(^{ + completionHandler([QNResponseInfo responseInfoWithInvalidToken:@"invalid token"], key, nil); + }); + return; + } + + [_config.zone preQuery:t on:^(int code) { + if (code != 0) { + QNAsyncRunInMain(^{ + completionHandler([QNResponseInfo responseInfoWithInvalidToken:@"get zone failed"], key, nil); + }); + return; + } + if ([data length] == 0) { + QNAsyncRunInMain(^{ + completionHandler([QNResponseInfo responseInfoOfZeroData:nil], key, nil); + }); + return; + } + QNUpCompletionHandler complete = ^(QNResponseInfo *info, NSString *key, NSDictionary *resp) { + QNAsyncRunInMain(^{ + completionHandler(info, key, resp); + }); + }; + QNFormUpload *up = [[QNFormUpload alloc] + initWithData:data + withKey:key + withToken:t + withCompletionHandler:complete + withOption:option + withHttpManager:_httpManager + withConfiguration:_config]; + QNAsyncRun(^{ + [up put]; + }); + }]; +} + +- (void)putFileInternal:(id)file + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option { + @autoreleasepool { + QNUpToken *t = [QNUpToken parse:token]; + if (t == nil) { + QNAsyncRunInMain(^{ + completionHandler([QNResponseInfo responseInfoWithInvalidToken:@"invalid token"], key, nil); + }); + return; + } + + [_config.zone preQuery:t on:^(int code) { + if (code != 0) { + QNAsyncRunInMain(^{ + completionHandler([QNResponseInfo responseInfoWithInvalidToken:@"get zone failed"], key, nil); + }); + return; + } + QNUpCompletionHandler complete = ^(QNResponseInfo *info, NSString *key, NSDictionary *resp) { + [file close]; + QNAsyncRunInMain(^{ + completionHandler(info, key, resp); + }); + }; + + if ([file size] <= _config.putThreshold) { + NSData *data = [file readAll]; + [self putData:data key:key token:token complete:complete option:option]; + return; + } + + NSString *recorderKey = key; + if (_config.recorder != nil && _config.recorderKeyGen != nil) { + recorderKey = _config.recorderKeyGen(key, [file path]); + } + + NSLog(@"recorder %@", _config.recorder); + + QNResumeUpload *up = [[QNResumeUpload alloc] + initWithFile:file + withKey:key + withToken:t + withCompletionHandler:complete + withOption:option + withRecorder:_config.recorder + withRecorderKey:recorderKey + withHttpManager:_httpManager + withConfiguration:_config]; + QNAsyncRun(^{ + [up run]; + }); + }]; + } +} + +- (void)putFile:(NSString *)filePath + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option { + if ([QNUploadManager checkAndNotifyError:key token:token input:filePath complete:completionHandler]) { + return; + } + + @autoreleasepool { + NSError *error = nil; + __block QNFile *file = [[QNFile alloc] init:filePath error:&error]; + if (error) { + QNAsyncRunInMain(^{ + QNResponseInfo *info = [QNResponseInfo responseInfoWithFileError:error]; + completionHandler(info, key, nil); + }); + return; + } + [self putFileInternal:file key:key token:token complete:completionHandler option:option]; + } +} + +- (void)putALAsset:(ALAsset *)asset + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option { +#if __IPHONE_OS_VERSION_MIN_REQUIRED + if ([QNUploadManager checkAndNotifyError:key token:token input:asset complete:completionHandler]) { + return; + } + + @autoreleasepool { + NSError *error = nil; + __block QNALAssetFile *file = [[QNALAssetFile alloc] init:asset error:&error]; + if (error) { + QNAsyncRunInMain(^{ + QNResponseInfo *info = [QNResponseInfo responseInfoWithFileError:error]; + completionHandler(info, key, nil); + }); + return; + } + [self putFileInternal:file key:key token:token complete:completionHandler option:option]; + } +#endif +} + +- (void)putPHAsset:(PHAsset *)asset + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option { +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000) + if ([QNUploadManager checkAndNotifyError:key token:token input:asset complete:completionHandler]) { + return; + } + + @autoreleasepool { + NSError *error = nil; + __block QNPHAssetFile *file = [[QNPHAssetFile alloc] init:asset error:&error]; + if (error) { + QNAsyncRunInMain(^{ + QNResponseInfo *info = [QNResponseInfo responseInfoWithFileError:error]; + completionHandler(info, key, nil); + }); + return; + } + [self putFileInternal:file key:key token:token complete:completionHandler option:option]; + } +#endif +} + +- (void)putPHAssetResource:(PHAssetResource *)assetResource + key:(NSString *)key + token:(NSString *)token + complete:(QNUpCompletionHandler)completionHandler + option:(QNUploadOption *)option { +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90100) + if ([QNUploadManager checkAndNotifyError:key token:token input:assetResource complete:completionHandler]) { + return; + } + @autoreleasepool { + NSError *error = nil; + __block QNPHAssetResource *file = [[QNPHAssetResource alloc] init:assetResource error:&error]; + if (error) { + QNAsyncRunInMain(^{ + QNResponseInfo *info = [QNResponseInfo responseInfoWithFileError:error]; + completionHandler(info, key, nil); + }); + return; + } + [self putFileInternal:file key:key token:token complete:completionHandler option:option]; + } +#endif +} + +@end diff --git a/msext/QiniuSDK/Storage/QNUploadOption+Private.h b/msext/QiniuSDK/Storage/QNUploadOption+Private.h new file mode 100755 index 0000000..370d05f --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUploadOption+Private.h @@ -0,0 +1,14 @@ +// +// QNUploadOption+Private.h +// QiniuSDK +// +// Created by bailong on 14/10/5. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNUploadOption.h" + +@interface QNUploadOption (Private) + +@property (nonatomic, getter=priv_isCancelled, readonly) BOOL cancelled; +@end diff --git a/msext/QiniuSDK/Storage/QNUploadOption.h b/msext/QiniuSDK/Storage/QNUploadOption.h new file mode 100755 index 0000000..3e941b6 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUploadOption.h @@ -0,0 +1,83 @@ +// +// QNUploadOption.h +// QiniuSDK +// +// Created by bailong on 14/10/4. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import + +/** + * 上传进度回调函数 + * + * @param key 上传时指定的存储key + * @param percent 进度百分比 + */ +typedef void (^QNUpProgressHandler)(NSString *key, float percent); + +/** + * 上传中途取消函数 + * + * @return 如果想取消,返回True, 否则返回No + */ +typedef BOOL (^QNUpCancellationSignal)(void); + +/** + * 可选参数集合,此类初始化后sdk上传使用时 不会对此进行改变;如果参数没有变化以及没有使用依赖,可以重复使用。 + */ +@interface QNUploadOption : NSObject + +/** + * 用于服务器上传回调通知的自定义参数,参数的key必须以x: 开头 + */ +@property (copy, nonatomic, readonly) NSDictionary *params; + +/** + * 指定文件的mime类型 + */ +@property (copy, nonatomic, readonly) NSString *mimeType; + +/** + * 是否进行crc校验 + */ +@property (readonly) BOOL checkCrc; + +/** + * 进度回调函数 + */ +@property (copy, readonly) QNUpProgressHandler progressHandler; + +/** + * 中途取消函数 + */ +@property (copy, readonly) QNUpCancellationSignal cancellationSignal; + +/** + * 可选参数的初始化方法 + * + * @param mimeType mime类型 + * @param progress 进度函数 + * @param params 自定义服务器回调参数 + * @param check 是否进行crc检查 + * @param cancellation 中途取消函数 + * + * @return 可选参数类实例 + */ +- (instancetype)initWithMime:(NSString *)mimeType + progressHandler:(QNUpProgressHandler)progress + params:(NSDictionary *)params + checkCrc:(BOOL)check + cancellationSignal:(QNUpCancellationSignal)cancellation; + +- (instancetype)initWithProgessHandler:(QNUpProgressHandler)progress DEPRECATED_ATTRIBUTE; +- (instancetype)initWithProgressHandler:(QNUpProgressHandler)progress; + +/** + * 内部使用,默认的参数实例 + * + * @return 可选参数类实例 + */ ++ (instancetype)defaultOptions; + +@end diff --git a/msext/QiniuSDK/Storage/QNUploadOption.m b/msext/QiniuSDK/Storage/QNUploadOption.m new file mode 100755 index 0000000..95857c5 --- /dev/null +++ b/msext/QiniuSDK/Storage/QNUploadOption.m @@ -0,0 +1,67 @@ +// +// QNUploadOption.m +// QiniuSDK +// +// Created by bailong on 14/10/4. +// Copyright (c) 2014年 Qiniu. All rights reserved. +// + +#import "QNUploadOption+Private.h" +#import "QNUploadManager.h" + +static NSString *mime(NSString *mimeType) { + if (mimeType == nil || [mimeType isEqualToString:@""]) { + return @"application/octet-stream"; + } + return mimeType; +} + +@implementation QNUploadOption + ++ (NSDictionary *)filteParam:(NSDictionary *)params { + NSMutableDictionary *ret = [NSMutableDictionary dictionary]; + if (params == nil) { + return ret; + } + + [params enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { + if ([key hasPrefix:@"x:"] && ![obj isEqualToString:@""]) { + ret[key] = obj; + } + }]; + + return ret; +} + +- (instancetype)initWithProgessHandler:(QNUpProgressHandler)progress { + return [self initWithMime:nil progressHandler:progress params:nil checkCrc:NO cancellationSignal:nil]; +} + +- (instancetype)initWithProgressHandler:(QNUpProgressHandler)progress { + return [self initWithMime:nil progressHandler:progress params:nil checkCrc:NO cancellationSignal:nil]; +} + +- (instancetype)initWithMime:(NSString *)mimeType + progressHandler:(QNUpProgressHandler)progress + params:(NSDictionary *)params + checkCrc:(BOOL)check + cancellationSignal:(QNUpCancellationSignal)cancel { + if (self = [super init]) { + _mimeType = mime(mimeType); + _progressHandler = progress != nil ? progress : ^(NSString *key, float percent) { + }; + _params = [QNUploadOption filteParam:params]; + _checkCrc = check; + _cancellationSignal = cancel != nil ? cancel : ^BOOL() { + return NO; + }; + } + + return self; +} + ++ (instancetype)defaultOptions { + return [[QNUploadOption alloc] initWithMime:nil progressHandler:nil params:nil checkCrc:NO cancellationSignal:nil]; +} + +@end diff --git a/msext/Res/BackBT.png b/msext/Res/BackBT.png new file mode 100755 index 0000000..2718a1f Binary files /dev/null and b/msext/Res/BackBT.png differ diff --git a/msext/Res/Default-568h@2x~iphone.png b/msext/Res/Default-568h@2x~iphone.png new file mode 100755 index 0000000..448b070 Binary files /dev/null and b/msext/Res/Default-568h@2x~iphone.png differ diff --git a/msext/Res/Icon180.png b/msext/Res/Icon180.png new file mode 100755 index 0000000..a814abb Binary files /dev/null and b/msext/Res/Icon180.png differ diff --git a/msext/Res/Sistem_back.png b/msext/Res/Sistem_back.png new file mode 100755 index 0000000..9f0e9b7 Binary files /dev/null and b/msext/Res/Sistem_back.png differ diff --git a/msext/Res/shake_sound_male.mp3 b/msext/Res/shake_sound_male.mp3 new file mode 100755 index 0000000..b18210d Binary files /dev/null and b/msext/Res/shake_sound_male.mp3 differ diff --git a/msext/Res/sharelogo.png b/msext/Res/sharelogo.png new file mode 100755 index 0000000..98a53d9 Binary files /dev/null and b/msext/Res/sharelogo.png differ diff --git a/msext/ViewController.m b/msext/ViewController.m deleted file mode 100644 index e86f7ad..0000000 --- a/msext/ViewController.m +++ /dev/null @@ -1,27 +0,0 @@ -// -// ViewController.m -// msext -// -// Created by chao on 15/7/23. -// Copyright (c) 2015年 chao. All rights reserved. -// - -#import "ViewController.h" - -@interface ViewController () - -@end - -@implementation ViewController - -- (void)viewDidLoad { - [super viewDidLoad]; - // Do any additional setup after loading the view, typically from a nib. -} - -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - -@end diff --git a/msext/gamehall.zip b/msext/gamehall.zip new file mode 100644 index 0000000..f07681d Binary files /dev/null and b/msext/gamehall.zip differ diff --git a/msext/gamehall/version.js b/msext/gamehall/version.js new file mode 100755 index 0000000..639b7e0 --- /dev/null +++ b/msext/gamehall/version.js @@ -0,0 +1,11 @@ + +GameData.AgentId = "i33v0llvp0euhd1n9qo1fM2RV8vtog4y"; +GameData.ChannelId = "7N0e0z2u2098pf1M2fj0kyB1D4n4ylkA"; +GameData.GameId = "8x4l0rGjf026f60c48h0mbUAhK5vV16f"; +GameData.Version="1.0";//真实版本号:2代理商:天盛网络 游戏名:牛牛 +GameData.versionCode = 1; + + + + + diff --git a/msext/gamehall/version.xml b/msext/gamehall/version.xml new file mode 100755 index 0000000..ca1fbd8 --- /dev/null +++ b/msext/gamehall/version.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/msext/main.m b/msext/main.m old mode 100644 new mode 100755 diff --git a/msext/msext-Prefix.pch b/msext/msext-Prefix.pch new file mode 100755 index 0000000..94520db --- /dev/null +++ b/msext/msext-Prefix.pch @@ -0,0 +1,26 @@ +// +// msext-Prefix.pch +// msext +// +// Created by chao on 15/7/23. +// Copyright (c) 2015年 chao. All rights reserved. +// + +#ifndef msext_msext_Prefix_pch +#define msext_msext_Prefix_pch + +// Include any system framework and library headers here that should be included in all compilation units. +// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. + +#endif + +#ifdef __OBJC__ + +#import + +#import +#import "FuncPublic.h" +#import "SGDefineInfo.h" +#import "UpNavigationBar.h" +#import "SGGateway.h" +#endif \ No newline at end of file diff --git a/msext/msext.entitlements b/msext/msext.entitlements new file mode 100755 index 0000000..903def2 --- /dev/null +++ b/msext/msext.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/msextTests/Info.plist b/msextTests/Info.plist old mode 100644 new mode 100755 index b610742..77af1ca --- a/msextTests/Info.plist +++ b/msextTests/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.chao.$(PRODUCT_NAME:rfc1034identifier) + com.skyapp.ylniuniu CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/msextTests/msextTests.m b/msextTests/msextTests.m old mode 100644 new mode 100755