From 40ef06c9d2118a3f0d92311170cfc3d57d06b294 Mon Sep 17 00:00:00 2001 From: Stefan Reitshamer Date: Mon, 20 Aug 2012 10:00:49 -0400 Subject: [PATCH] Switched from NSURLConnection API to CFHTTP API. I made this switch in Arq 2.6.3 last March, and it has been much more stable. --- BackupSet.m | 3 +- arq_restore.xcodeproj/project.pbxproj | 90 +++++ http/CFHTTPConnection.h | 83 ++++ http/CFHTTPConnection.m | 539 ++++++++++++++++++++++++++ http/CFHTTPInputStream.h | 28 ++ http/CFHTTPInputStream.m | 135 +++++++ http/CFNetwork.h | 41 ++ http/CFNetwork.m | 130 +++++++ http/HTTPConnectionDelegate.h | 19 + http/HTTPConnectionFactory.h | 27 ++ http/HTTPConnectionFactory.m | 163 ++++++++ http/HTTPTimeoutSetting.h | 16 + http/HTTPTimeoutSetting.m | 35 ++ io/NetMonitor.h | 48 +++ io/NetMonitor.m | 132 +++++++ s3/RemoteS3Signer.h | 20 + s3/RemoteS3Signer.m | 75 ++++ s3/S3AuthorizationProvider.h | 6 +- s3/S3AuthorizationProvider.m | 33 +- s3/S3Lister.m | 9 +- s3/S3Owner.h | 43 ++ s3/S3Owner.m | 50 +++ s3/S3Region.h | 39 ++ s3/S3Region.m | 162 ++++++++ s3/S3Request.h | 17 +- s3/S3Request.m | 162 ++++---- s3/S3Service.h | 17 +- s3/S3Service.m | 130 ++++--- shared/DNS_SDErrors.h | 40 ++ shared/DNS_SDErrors.m | 101 +++++ shared/NSErrorCodes.h | 6 +- shared/NSError_extra.h | 3 + shared/NSError_extra.m | 89 ++++- shared/Sysctl.h | 40 ++ shared/Sysctl.m | 78 ++++ 35 files changed, 2436 insertions(+), 173 deletions(-) create mode 100644 http/CFHTTPConnection.h create mode 100644 http/CFHTTPConnection.m create mode 100644 http/CFHTTPInputStream.h create mode 100644 http/CFHTTPInputStream.m create mode 100644 http/CFNetwork.h create mode 100644 http/CFNetwork.m create mode 100644 http/HTTPConnectionDelegate.h create mode 100644 http/HTTPConnectionFactory.h create mode 100644 http/HTTPConnectionFactory.m create mode 100644 http/HTTPTimeoutSetting.h create mode 100644 http/HTTPTimeoutSetting.m create mode 100644 io/NetMonitor.h create mode 100644 io/NetMonitor.m create mode 100644 s3/RemoteS3Signer.h create mode 100644 s3/RemoteS3Signer.m create mode 100644 s3/S3Owner.h create mode 100644 s3/S3Owner.m create mode 100644 s3/S3Region.h create mode 100644 s3/S3Region.m create mode 100644 shared/DNS_SDErrors.h create mode 100644 shared/DNS_SDErrors.m create mode 100644 shared/Sysctl.h create mode 100644 shared/Sysctl.m diff --git a/BackupSet.m b/BackupSet.m index 360da27..6f0e810 100644 --- a/BackupSet.m +++ b/BackupSet.m @@ -44,6 +44,7 @@ #import "Commit.h" #import "S3ObjectMetadata.h" #import "ArqSalt.h" +#import "S3Region.h" @implementation BackupSet + (NSArray *)allBackupSetsForAccessKeyID:(NSString *)theAccessKeyID secretAccessKey:(NSString *)theSecretAccessKey error:(NSError **)error { @@ -125,7 +126,7 @@ #pragma mark NSObject - (NSString *)description { - NSString *bucketRegion = [S3Service displayNameForBucketRegion:[S3Service s3BucketRegionForS3BucketName:s3BucketName]]; + NSString *bucketRegion = [[S3Region s3RegionForBucketName:s3BucketName] displayName]; if (uac != nil) { return [NSString stringWithFormat:@"%@ (%@) : %@ (%@)", [uac computerName], [uac userName], bucketRegion, computerUUID]; } diff --git a/arq_restore.xcodeproj/project.pbxproj b/arq_restore.xcodeproj/project.pbxproj index ac55c7d..6da9438 100644 --- a/arq_restore.xcodeproj/project.pbxproj +++ b/arq_restore.xcodeproj/project.pbxproj @@ -131,6 +131,28 @@ F83C1AD211CA7C170001958F /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F805B8CD1160ECD7007EC01E /* CoreServices.framework */; }; F83C1AE311CA7C7C0001958F /* arq_restore.m in Sources */ = {isa = PBXBuildFile; fileRef = 08FB7796FE84155DC02AAC07 /* arq_restore.m */; }; F83C1D1D11CA95AF0001958F /* BucketVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = F83C1D0911CA929D0001958F /* BucketVerifier.m */; }; + F84166D815E2782600B6ECED /* HTTPConnectionFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166D515E2782600B6ECED /* HTTPConnectionFactory.m */; }; + F84166D915E2782600B6ECED /* HTTPTimeoutSetting.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166D715E2782600B6ECED /* HTTPTimeoutSetting.m */; }; + F84166DA15E2782600B6ECED /* HTTPConnectionFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166D515E2782600B6ECED /* HTTPConnectionFactory.m */; }; + F84166DB15E2782600B6ECED /* HTTPTimeoutSetting.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166D715E2782600B6ECED /* HTTPTimeoutSetting.m */; }; + F84166E215E2785100B6ECED /* CFHTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166DF15E2785100B6ECED /* CFHTTPConnection.m */; }; + F84166E315E2785100B6ECED /* CFHTTPInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166E115E2785100B6ECED /* CFHTTPInputStream.m */; }; + F84166E415E2785100B6ECED /* CFHTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166DF15E2785100B6ECED /* CFHTTPConnection.m */; }; + F84166E515E2785100B6ECED /* CFHTTPInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166E115E2785100B6ECED /* CFHTTPInputStream.m */; }; + F84166ED15E278BC00B6ECED /* CFNetwork.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166EC15E278BC00B6ECED /* CFNetwork.m */; }; + F84166EE15E278BC00B6ECED /* CFNetwork.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166EC15E278BC00B6ECED /* CFNetwork.m */; }; + F84166F515E278F500B6ECED /* S3Owner.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166F215E278F500B6ECED /* S3Owner.m */; }; + F84166F615E278F500B6ECED /* S3Region.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166F415E278F500B6ECED /* S3Region.m */; }; + F84166F715E278F500B6ECED /* S3Owner.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166F215E278F500B6ECED /* S3Owner.m */; }; + F84166F815E278F500B6ECED /* S3Region.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166F415E278F500B6ECED /* S3Region.m */; }; + F84166FD15E2792500B6ECED /* NetMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166FC15E2792500B6ECED /* NetMonitor.m */; }; + F84166FE15E2792500B6ECED /* NetMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = F84166FC15E2792500B6ECED /* NetMonitor.m */; }; + F841670515E279BB00B6ECED /* Sysctl.m in Sources */ = {isa = PBXBuildFile; fileRef = F841670415E279BB00B6ECED /* Sysctl.m */; }; + F841670615E279BB00B6ECED /* Sysctl.m in Sources */ = {isa = PBXBuildFile; fileRef = F841670415E279BB00B6ECED /* Sysctl.m */; }; + F841670B15E279DE00B6ECED /* DNS_SDErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = F841670A15E279DE00B6ECED /* DNS_SDErrors.m */; }; + F841670C15E279DE00B6ECED /* DNS_SDErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = F841670A15E279DE00B6ECED /* DNS_SDErrors.m */; }; + F841672615E27A5200B6ECED /* RemoteS3Signer.m in Sources */ = {isa = PBXBuildFile; fileRef = F841672515E27A5200B6ECED /* RemoteS3Signer.m */; }; + F841672715E27A5200B6ECED /* RemoteS3Signer.m in Sources */ = {isa = PBXBuildFile; fileRef = F841672515E27A5200B6ECED /* RemoteS3Signer.m */; }; F8987235121EB68900F07D76 /* BinaryPListReader.m in Sources */ = {isa = PBXBuildFile; fileRef = F8987232121EB68900F07D76 /* BinaryPListReader.m */; }; F8987236121EB68900F07D76 /* BinaryPListWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = F8987234121EB68900F07D76 /* BinaryPListWriter.m */; }; F8987560121EBD9600F07D76 /* BufferedInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = F898755F121EBD9600F07D76 /* BufferedInputStream.m */; }; @@ -351,6 +373,29 @@ F83C1AD711CA7C170001958F /* arq_verify */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = arq_verify; sourceTree = BUILT_PRODUCTS_DIR; }; F83C1D0811CA929D0001958F /* BucketVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BucketVerifier.h; path = s3/BucketVerifier.h; sourceTree = ""; }; F83C1D0911CA929D0001958F /* BucketVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BucketVerifier.m; path = s3/BucketVerifier.m; sourceTree = ""; }; + F84166D415E2782600B6ECED /* HTTPConnectionFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPConnectionFactory.h; sourceTree = ""; }; + F84166D515E2782600B6ECED /* HTTPConnectionFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPConnectionFactory.m; sourceTree = ""; }; + F84166D615E2782600B6ECED /* HTTPTimeoutSetting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPTimeoutSetting.h; sourceTree = ""; }; + F84166D715E2782600B6ECED /* HTTPTimeoutSetting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPTimeoutSetting.m; sourceTree = ""; }; + F84166DE15E2785100B6ECED /* CFHTTPConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CFHTTPConnection.h; sourceTree = ""; }; + F84166DF15E2785100B6ECED /* CFHTTPConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CFHTTPConnection.m; sourceTree = ""; }; + F84166E015E2785100B6ECED /* CFHTTPInputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CFHTTPInputStream.h; sourceTree = ""; }; + F84166E115E2785100B6ECED /* CFHTTPInputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CFHTTPInputStream.m; sourceTree = ""; }; + F84166E815E2787B00B6ECED /* HTTPConnectionDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPConnectionDelegate.h; sourceTree = ""; }; + F84166EB15E278BC00B6ECED /* CFNetwork.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CFNetwork.h; sourceTree = ""; }; + F84166EC15E278BC00B6ECED /* CFNetwork.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CFNetwork.m; sourceTree = ""; }; + F84166F115E278F500B6ECED /* S3Owner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = S3Owner.h; sourceTree = ""; }; + F84166F215E278F500B6ECED /* S3Owner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = S3Owner.m; sourceTree = ""; }; + F84166F315E278F500B6ECED /* S3Region.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = S3Region.h; sourceTree = ""; }; + F84166F415E278F500B6ECED /* S3Region.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = S3Region.m; sourceTree = ""; }; + F84166FB15E2792500B6ECED /* NetMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetMonitor.h; sourceTree = ""; }; + F84166FC15E2792500B6ECED /* NetMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetMonitor.m; sourceTree = ""; }; + F841670315E279BB00B6ECED /* Sysctl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Sysctl.h; sourceTree = ""; }; + F841670415E279BB00B6ECED /* Sysctl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Sysctl.m; sourceTree = ""; }; + F841670915E279DE00B6ECED /* DNS_SDErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DNS_SDErrors.h; sourceTree = ""; }; + F841670A15E279DE00B6ECED /* DNS_SDErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DNS_SDErrors.m; sourceTree = ""; }; + F841672415E27A5200B6ECED /* RemoteS3Signer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RemoteS3Signer.h; sourceTree = ""; }; + F841672515E27A5200B6ECED /* RemoteS3Signer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RemoteS3Signer.m; sourceTree = ""; }; F8987231121EB68900F07D76 /* BinaryPListReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BinaryPListReader.h; sourceTree = ""; }; F8987232121EB68900F07D76 /* BinaryPListReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BinaryPListReader.m; sourceTree = ""; }; F8987233121EB68900F07D76 /* BinaryPListWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BinaryPListWriter.h; sourceTree = ""; }; @@ -622,6 +667,12 @@ F805B7651160DD60007EC01E /* s3 */ = { isa = PBXGroup; children = ( + F841672415E27A5200B6ECED /* RemoteS3Signer.h */, + F841672515E27A5200B6ECED /* RemoteS3Signer.m */, + F84166F115E278F500B6ECED /* S3Owner.h */, + F84166F215E278F500B6ECED /* S3Owner.m */, + F84166F315E278F500B6ECED /* S3Region.h */, + F84166F415E278F500B6ECED /* S3Region.m */, F89A205113FAE2DA0071D321 /* LocalS3Signer.h */, F89A205213FAE2DA0071D321 /* LocalS3Signer.m */, F805B76A1160DD60007EC01E /* NSError_S3.h */, @@ -649,6 +700,10 @@ F805B7A61160DEF2007EC01E /* shared */ = { isa = PBXGroup; children = ( + F841670915E279DE00B6ECED /* DNS_SDErrors.h */, + F841670A15E279DE00B6ECED /* DNS_SDErrors.m */, + F841670315E279BB00B6ECED /* Sysctl.h */, + F841670415E279BB00B6ECED /* Sysctl.m */, F89A1FD013FAD6BE0071D321 /* HSLog.h */, F89A1FD113FAD6BE0071D321 /* HSLog.m */, F89A1F4B13FAC73D0071D321 /* Computer.h */, @@ -690,6 +745,17 @@ F805B7C91160E445007EC01E /* http */ = { isa = PBXGroup; children = ( + F84166EB15E278BC00B6ECED /* CFNetwork.h */, + F84166EC15E278BC00B6ECED /* CFNetwork.m */, + F84166E815E2787B00B6ECED /* HTTPConnectionDelegate.h */, + F84166DE15E2785100B6ECED /* CFHTTPConnection.h */, + F84166DF15E2785100B6ECED /* CFHTTPConnection.m */, + F84166E015E2785100B6ECED /* CFHTTPInputStream.h */, + F84166E115E2785100B6ECED /* CFHTTPInputStream.m */, + F84166D415E2782600B6ECED /* HTTPConnectionFactory.h */, + F84166D515E2782600B6ECED /* HTTPConnectionFactory.m */, + F84166D615E2782600B6ECED /* HTTPTimeoutSetting.h */, + F84166D715E2782600B6ECED /* HTTPTimeoutSetting.m */, F89A1EEA13FAC4E30071D321 /* URLConnection.h */, F89A1EEB13FAC4E30071D321 /* URLConnection.m */, F805B7CA1160E445007EC01E /* HTTP.h */, @@ -703,6 +769,8 @@ F805B8081160E7A1007EC01E /* io */ = { isa = PBXGroup; children = ( + F84166FB15E2792500B6ECED /* NetMonitor.h */, + F84166FC15E2792500B6ECED /* NetMonitor.m */, F89A205713FAE3010071D321 /* DataOutputStream.h */, F89A205813FAE3010071D321 /* DataOutputStream.m */, F89A1F6113FAC8270071D321 /* CryptoKey.h */, @@ -995,6 +1063,17 @@ F81426D714541A6C00D7E50A /* BackupSet.m in Sources */, F8373E0F14794D01005AFBE6 /* ReflogEntry.m in Sources */, F8373E5A147A8DEC005AFBE6 /* ReflogPrinter.m in Sources */, + F84166D815E2782600B6ECED /* HTTPConnectionFactory.m in Sources */, + F84166D915E2782600B6ECED /* HTTPTimeoutSetting.m in Sources */, + F84166E215E2785100B6ECED /* CFHTTPConnection.m in Sources */, + F84166E315E2785100B6ECED /* CFHTTPInputStream.m in Sources */, + F84166ED15E278BC00B6ECED /* CFNetwork.m in Sources */, + F84166F515E278F500B6ECED /* S3Owner.m in Sources */, + F84166F615E278F500B6ECED /* S3Region.m in Sources */, + F84166FD15E2792500B6ECED /* NetMonitor.m in Sources */, + F841670515E279BB00B6ECED /* Sysctl.m in Sources */, + F841670B15E279DE00B6ECED /* DNS_SDErrors.m in Sources */, + F841672615E27A5200B6ECED /* RemoteS3Signer.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1099,6 +1178,17 @@ F89A205313FAE2DA0071D321 /* LocalS3Signer.m in Sources */, F89A205913FAE3010071D321 /* DataOutputStream.m in Sources */, F89A20CF13FAE7170071D321 /* NSError_extra.m in Sources */, + F84166DA15E2782600B6ECED /* HTTPConnectionFactory.m in Sources */, + F84166DB15E2782600B6ECED /* HTTPTimeoutSetting.m in Sources */, + F84166E415E2785100B6ECED /* CFHTTPConnection.m in Sources */, + F84166E515E2785100B6ECED /* CFHTTPInputStream.m in Sources */, + F84166EE15E278BC00B6ECED /* CFNetwork.m in Sources */, + F84166F715E278F500B6ECED /* S3Owner.m in Sources */, + F84166F815E278F500B6ECED /* S3Region.m in Sources */, + F84166FE15E2792500B6ECED /* NetMonitor.m in Sources */, + F841670615E279BB00B6ECED /* Sysctl.m in Sources */, + F841670C15E279DE00B6ECED /* DNS_SDErrors.m in Sources */, + F841672715E27A5200B6ECED /* RemoteS3Signer.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/http/CFHTTPConnection.h b/http/CFHTTPConnection.h new file mode 100644 index 0000000..de815c6 --- /dev/null +++ b/http/CFHTTPConnection.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2009-2010, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "HTTPConnection.h" +@protocol HTTPConnectionDelegate; +#import "InputStream.h" +@class RFC2616DateFormatter; +@class HTTPTimeoutSetting; + +@interface CFHTTPConnection : NSObject { + RFC2616DateFormatter *dateFormatter; + NSURL *url; + NSString *requestMethod; + id httpConnectionDelegate; + NSMutableDictionary *requestHeaders; + CFHTTPMessageRef request; + NSInputStream *readStream; + BOOL errorOccurred; + NSError *_error; + BOOL complete; + BOOL hasBytesAvailable; + int responseStatusCode; + NSDictionary *responseHeaders; + NSTimeInterval createTime; + BOOL closeRequested; + CFHTTPConnection *previous; + NSDate *sendTimeout; + unsigned long long totalSent; + HTTPTimeoutSetting *httpTimeoutSetting; +} ++ (NSString *)errorDomain; +- (id)initWithURL:(NSURL *)theURL method:(NSString *)theMethod httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting httpConnectionDelegate:(id )theDelegate; +- (id)initWithURL:(NSURL *)theURL method:(NSString *)theMethod httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting httpConnectionDelegate:(id )theDelegate previousConnection:(CFHTTPConnection *)thePrevious; +- (void)setRequestHeader:(NSString *)value forKey:(NSString *)key; +- (void)setRequestHostHeader; +- (void)setRequestContentDispositionHeader:(NSString *)downloadName; +- (void)setRFC822DateRequestHeader; +- (NSString *)requestMethod; +- (NSString *)requestPathInfo; +- (NSString *)requestQueryString; +- (NSArray *)requestHeaderKeys; +- (NSString *)requestHeaderForKey:(NSString *)theKey; +- (BOOL)executeRequest:(NSError **)error; +- (BOOL)executeRequestWithBody:(NSData *)requestBody error:(NSError **)error; +- (int)responseCode; +- (NSString *)responseHeaderForKey:(NSString *)key; +- (NSString *)responseContentType; +- (NSString *)responseDownloadName; +- (id )newResponseBodyStream:(NSError **)error; +- (void)setCloseRequested; +- (BOOL)isCloseRequested; +- (NSTimeInterval)createTime; +- (void)releasePreviousConnection; +@end diff --git a/http/CFHTTPConnection.m b/http/CFHTTPConnection.m new file mode 100644 index 0000000..b62c07b --- /dev/null +++ b/http/CFHTTPConnection.m @@ -0,0 +1,539 @@ +/* + Copyright (c) 2009-2010, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "CFHTTPConnection.h" +#import "BufferedInputStream.h" +#import "HSLog.h" +#import "OutputStream.h" +#import "RFC2616DateFormatter.h" +#import "RegexKitLite.h" +#import "HTTP.h" +#import "SetNSError.h" +#import "ChunkedInputStream.h" +#import "FixedLengthInputStream.h" +#import "NSData-InputStream.h" +#import "DataInputStream.h" +#import "CFNetwork.h" +#import +#import "CFNetwork.h" +#import "InputStreams.h" +#import "CFHTTPInputStream.h" +#import "NSErrorCodes.h" +#import "HTTPConnectionDelegate.h" +#import "HTTPTimeoutSetting.h" + + +#define DEFAULT_TIMEOUT_SECONDS (30.0) +#define MY_BUF_SIZE (8192) +static NSString *runLoopMode = @"HTTPConnectionRunLoopMode"; + +@interface CFHTTPConnection (usability) +- (BOOL)isUsable; +@end + +@interface CFHTTPConnection (internal) +- (void)doExecuteRequestWithBody:(NSData *)requestBody; +- (void)setProxiesOnReadStream; +- (void)handleNetworkEvent:(CFStreamEventType)theType; +- (void)handleBytesAvailable; +- (void)handleStreamComplete; +- (void)handleStreamError; +- (void)readResponseHeaders; +- (void)destroyReadStream; +- (void)resetSendTimeout; +- (id )doNewResponseBodyStream:(NSError **)error; +- (NSInteger)doRead:(unsigned char *)buf bufferLength:(NSUInteger)length error:(NSError **)error; +@end + +@interface CFHTTPConnection (callback) +- (void)sentRequestBytes:(NSInteger)count; +@end + + +static void ReadStreamClientCallback(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) { + [(CFHTTPConnection *)clientCallBackInfo handleNetworkEvent:type]; +} + +@implementation CFHTTPConnection ++ (NSString *)errorDomain { + return @"HTTPConnectionErrorDomain"; +} + +- (id)initWithURL:(NSURL *)theURL method:(NSString *)theMethod httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting httpConnectionDelegate:(id )theDelegate { + return [self initWithURL:theURL method:theMethod httpTimeoutSetting:theHTTPTimeoutSetting httpConnectionDelegate:theDelegate previousConnection:nil]; +} +- (id)initWithURL:(NSURL *)theURL method:(NSString *)theMethod httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting httpConnectionDelegate:(id )theDelegate previousConnection:(CFHTTPConnection *)thePrevious { + if (self = [super init]) { + dateFormatter = [[RFC2616DateFormatter alloc] init]; + url = [theURL retain]; + requestMethod = [theMethod retain]; + httpTimeoutSetting = [theHTTPTimeoutSetting retain]; + httpConnectionDelegate = theDelegate; + requestHeaders = [[NSMutableDictionary alloc] init]; + if (thePrevious != nil) { + previous = [thePrevious retain]; + createTime = [thePrevious createTime]; + } else { + createTime = [NSDate timeIntervalSinceReferenceDate]; + } + } + return self; +} +- (void)dealloc { + [dateFormatter release]; + [url release]; + [requestMethod release]; + [httpTimeoutSetting release]; + [requestHeaders release]; + if (request) { + CFRelease(request); + } + [runLoopMode release]; + [responseHeaders release]; + [self destroyReadStream]; + [previous release]; + [sendTimeout release]; + [super dealloc]; +} + +- (NSString *)errorDomain { + return @"HTTPConnectionErrorDomain"; +} + +- (void)setRequestHeader:(NSString *)value forKey:(NSString *)key { + [requestHeaders setObject:value forKey:key]; +} +- (void)setRequestHostHeader { + [self setRequestHeader:[url host] forKey:@"Host"]; +} +- (void)setRequestContentDispositionHeader:(NSString *)downloadName { + if (downloadName != nil) { + NSString *encodedFilename = [NSString stringWithFormat:@"\"%@\"", [downloadName stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\\\""]]; + encodedFilename = [encodedFilename stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; + NSString *contentDisposition = [NSString stringWithFormat:@"attachment;filename=%@", encodedFilename]; + [self setRequestHeader:contentDisposition forKey:@"Content-Disposition"]; + } +} +- (void)setRFC822DateRequestHeader { + [self setRequestHeader:[dateFormatter rfc2616StringFromDate:[NSDate date]] forKey:@"Date"]; +} +- (NSString *)requestMethod { + return requestMethod; +} +//- (NSString *)requestPathInfo { +// return [[url path] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; +//} +- (NSString *)requestPathInfo { + NSString *urlDescription = [url description]; + NSRange rangeBeforeQueryString = [urlDescription rangeOfRegex:@"^([^?]+)"]; + NSString *stringBeforeQueryString = [urlDescription substringWithRange:rangeBeforeQueryString]; + NSString *path = [url path]; + if ([stringBeforeQueryString hasSuffix:@"/"] && ![path hasSuffix:@"/"]) { + // NSURL's path method strips trailing slashes. Add it back in. + path = [path stringByAppendingString:@"/"]; + } + return [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; +} +- (NSString *)requestQueryString { + return [url query]; +} +- (NSArray *)requestHeaderKeys { + return [requestHeaders allKeys]; +} +- (NSString *)requestHeaderForKey:(NSString *)theKey { + return [requestHeaders objectForKey:theKey]; +} +- (BOOL)executeRequest:(NSError **)error { + return [self executeRequestWithBody:nil error:error]; +} +- (BOOL)executeRequestWithBody:(NSData *)requestBody error:(NSError **)error { + if (closeRequested) { + SETNSERROR([CFHTTPConnection errorDomain], -1, @"close was requested; can't reuse this connection"); + return NO; + } + [self doExecuteRequestWithBody:requestBody]; + if (errorOccurred) { + if (error != NULL) { + *error = _error; + } + if ([httpConnectionDelegate respondsToSelector:@selector(httpConnection:subtractSentBytes:)]) { + [httpConnectionDelegate httpConnection:self subtractSentBytes:totalSent]; + } + return NO; + } + return YES; +} +- (int)responseCode { + if (responseHeaders == nil) { + // User probably canceled before we received the response header. + return HTTP_INTERNAL_SERVER_ERROR; + } + return responseStatusCode; +} +- (NSString *)responseHeaderForKey:(NSString *)key { + return [responseHeaders objectForKey:key]; +} +- (NSString *)responseContentType { + return [self responseHeaderForKey:@"Content-Type"]; +} +- (NSString *)responseDownloadName { + NSString *downloadName = nil; + NSString *contentDisposition = [self responseHeaderForKey:@"Content-Disposition"]; + if (contentDisposition != nil) { + NSRange filenameRange = [contentDisposition rangeOfRegex:@"attachment;filename=(.+)" capture:1]; + if (filenameRange.location != NSNotFound) { + downloadName = [contentDisposition substringWithRange:filenameRange]; + } + } + return downloadName; +} +- (id )newResponseBodyStream:(NSError **)error { + id ret = [self doNewResponseBodyStream:error]; + if (ret == nil && [httpConnectionDelegate respondsToSelector:@selector(httpConnection:subtractSentBytes:)]) { + [httpConnectionDelegate httpConnection:self subtractSentBytes:totalSent]; + } + return ret; +} +- (void)setCloseRequested { + closeRequested = YES; +} +- (BOOL)isCloseRequested { + return closeRequested || errorOccurred; +} +- (NSTimeInterval)createTime { + return createTime; +} +- (void)releasePreviousConnection { + [previous release]; + previous = nil; +} + +#pragma mark InputStream +- (NSInteger)read:(unsigned char *)buf bufferLength:(NSUInteger)length error:(NSError **)error { + NSInteger ret = [self doRead:buf bufferLength:length error:error]; + if (ret < 0 && [httpConnectionDelegate respondsToSelector:@selector(httpConnection:subtractSentBytes:)]) { + [httpConnectionDelegate httpConnection:self subtractSentBytes:totalSent]; + } + return ret; +} +- (NSData *)slurp:(NSError **)error { + return [InputStreams slurp:self error:error]; +} + +#pragma mark NSObject +- (NSString *)description { + return [NSString stringWithFormat:@"", url, requestMethod]; +} +@end + +@implementation CFHTTPConnection (internal) +- (void)doExecuteRequestWithBody:(NSData *)requestBody { + request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, (CFStringRef)requestMethod, (CFURLRef)url, kCFHTTPVersion1_1); + if (!request) { + errorOccurred = YES; + _error = [[NSError errorWithDomain:[CFNetwork errorDomain] code:-1 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error creating request", NSLocalizedDescriptionKey, nil]] retain]; + return; + } + + // Add headers. + for (NSString *header in [requestHeaders allKeys]) { + CFHTTPMessageSetHeaderFieldValue(request, (CFStringRef)header, (CFStringRef)[requestHeaders objectForKey:header]); + } + + // Add keep-alive header every time: + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Keep-Alive")); + + if ([requestBody length] > 0) { + CFHTTPInputStream *bodyStream = [[[CFHTTPInputStream alloc] initWithCFHTTPConnection:self data:requestBody httpConnectionDelegate:httpConnectionDelegate] autorelease]; + readStream = (NSInputStream *)CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request, (CFReadStreamRef)bodyStream); + } else { + readStream = (NSInputStream *)CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request); + } + + HSLogTrace(@"new readStream: %p", readStream); + if (!readStream) { + errorOccurred = YES; + _error = [[NSError errorWithDomain:[CFNetwork errorDomain] code:-1 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error creating read stream", NSLocalizedDescriptionKey, nil]] retain]; + return; + } + + if ([[[url scheme] lowercaseString] isEqualToString:@"https"]) { + NSMutableDictionary *sslProperties = [NSDictionary dictionaryWithObjectsAndKeys: + (NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL, kCFStreamSSLLevel, + kCFBooleanTrue, kCFStreamSSLAllowsExpiredCertificates, + kCFBooleanTrue, kCFStreamSSLAllowsExpiredRoots, + kCFBooleanTrue, kCFStreamSSLAllowsAnyRoot, + kCFBooleanFalse, kCFStreamSSLValidatesCertificateChain, + kCFNull, kCFStreamSSLPeerName, + nil]; + CFReadStreamSetProperty((CFReadStreamRef)readStream, kCFStreamPropertySSLSettings, sslProperties); + } + [self setProxiesOnReadStream]; + + // Attempt to reuse this connection. + CFReadStreamSetProperty((CFReadStreamRef)readStream, kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanTrue); + + CFStreamClientContext ctxt = { 0, self, NULL, NULL, NULL }; + CFReadStreamSetClient((CFReadStreamRef)readStream, kCFStreamEventHasBytesAvailable | kCFStreamEventEndEncountered | kCFStreamEventErrorOccurred, ReadStreamClientCallback, &ctxt); + [readStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:runLoopMode]; + if (!CFReadStreamOpen((CFReadStreamRef)readStream)) { + errorOccurred = YES; + _error = [[NSError errorWithDomain:[CFNetwork errorDomain] code:-1 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error opening read stream", NSLocalizedDescriptionKey, nil]] retain]; + return; + } + [previous releasePreviousConnection]; +} +- (void)setProxiesOnReadStream { + NSDictionary *proxySettings = (NSDictionary *)SCDynamicStoreCopyProxies(NULL); + NSArray *proxies = (NSArray *)CFNetworkCopyProxiesForURL((CFURLRef)url, (CFDictionaryRef)proxySettings); + if ([proxies count] > 0) { + NSDictionary *proxy = [proxies objectAtIndex:0]; + NSString *proxyType = [proxy objectForKey:(NSString *)kCFProxyTypeKey]; + if (![proxyType isEqualToString:(NSString *)kCFProxyTypeNone]) { + NSString *proxyHost = [proxy objectForKey:(NSString *)kCFProxyHostNameKey]; + int proxyPort = [[proxy objectForKey:(NSString *)kCFProxyPortNumberKey] intValue]; + NSString *hostKey; + NSString *portKey; + if ([proxyType isEqualToString:(NSString *)kCFProxyTypeSOCKS]) { + hostKey = (NSString *)kCFStreamPropertySOCKSProxyHost; + portKey = (NSString *)kCFStreamPropertySOCKSProxyPort; + } else { + hostKey = (NSString *)kCFStreamPropertyHTTPProxyHost; + portKey = (NSString *)kCFStreamPropertyHTTPProxyPort; + if ([[[url scheme] lowercaseString] isEqualToString:@"https"]) { + hostKey = (NSString *)kCFStreamPropertyHTTPSProxyHost; + portKey = (NSString *)kCFStreamPropertyHTTPSProxyPort; + } + } + // FIXME: Support proxy autconfiguration files (kCFProxyTypeAutoConfigurationURL) too! + NSDictionary *proxyToUse = [NSDictionary dictionaryWithObjectsAndKeys: + proxyHost, hostKey, + [NSNumber numberWithInt:proxyPort], portKey, + nil]; + if ([proxyType isEqualToString:(NSString *)kCFProxyTypeSOCKS]) { + CFReadStreamSetProperty((CFReadStreamRef)readStream, kCFStreamPropertySOCKSProxy, proxyToUse); + } else { + CFReadStreamSetProperty((CFReadStreamRef)readStream, kCFStreamPropertyHTTPProxy, proxyToUse); + } + } + } + [proxies release]; + [proxySettings release]; +} +- (void)handleNetworkEvent:(CFStreamEventType)theType { + switch (theType) { + case kCFStreamEventHasBytesAvailable: + [self handleBytesAvailable]; + break; + case kCFStreamEventEndEncountered: + [self handleStreamComplete]; + break; + case kCFStreamEventErrorOccurred: + [self handleStreamError]; + break; + default: + break; + } +} +- (void)handleBytesAvailable { + HSLogTrace(@"%@: handleBytesAvailable", self); + [self readResponseHeaders]; + + // Rarely, there aren't any data actually available. + if (!CFReadStreamHasBytesAvailable((CFReadStreamRef)readStream)) { + return; + } + hasBytesAvailable = YES; +} +- (void)handleStreamComplete { + HSLogTrace(@"%@: handleStreamComplete", self); + [self readResponseHeaders]; + complete = YES; +} +- (void)handleStreamError { + HSLogTrace(@"%@: handleStreamError", self); + if (errorOccurred) { + NSLog(@"already have an error!"); + return; + } + errorOccurred = YES; + _error = (NSError *)CFReadStreamCopyError((CFReadStreamRef)readStream); + complete = YES; + closeRequested = YES; +} +- (void)readResponseHeaders { + if (responseHeaders) { + return; + } + CFHTTPMessageRef message = (CFHTTPMessageRef)CFReadStreamCopyProperty((CFReadStreamRef)readStream, kCFStreamPropertyHTTPResponseHeader); + if (message) { + if (!CFHTTPMessageIsHeaderComplete(message)) { + HSLogDebug(@"%@: header not complete!", self); + } else { + [responseHeaders release]; + responseHeaders = (NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message); + responseStatusCode = (int)CFHTTPMessageGetResponseStatusCode(message); + } + CFRelease(message); + } +} +- (void)destroyReadStream { + if (readStream != nil) { + HSLogTrace(@"destroying readStream: %p", readStream); + CFReadStreamSetClient((CFReadStreamRef)readStream, kCFStreamEventNone, NULL, NULL); + [readStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:runLoopMode]; + [readStream close]; + [readStream release]; + readStream = nil; + } +} +- (void)resetSendTimeout { + NSTimeInterval timeoutSeconds = [httpTimeoutSetting timeoutSeconds]; + if (timeoutSeconds == 0) { + timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; + } + [sendTimeout release]; + sendTimeout = [[[NSDate date] addTimeInterval:timeoutSeconds] retain]; +} +- (id )doNewResponseBodyStream:(NSError **)error { + while (!complete && !hasBytesAvailable) { + [self resetSendTimeout]; + HSLogTrace(@"newResponseBodyStream: running the runloop until %f", [sendTimeout timeIntervalSinceReferenceDate]); + [[NSRunLoop currentRunLoop] runMode:runLoopMode beforeDate:sendTimeout]; + NSTimeInterval current = [NSDate timeIntervalSinceReferenceDate]; + if ((current + 0.1) > [sendTimeout timeIntervalSinceReferenceDate]) { + HSLogWarn(@"timeout waiting for response to %@ %@", requestMethod, url); + SETNSERROR([self errorDomain], ERROR_TIMEOUT, @"timeout while attempting to send HTTP request"); + closeRequested = YES; + return nil; + } else { + HSLogTrace(@"current time %f is not later than timeout %f; continuing", current, [sendTimeout timeIntervalSinceReferenceDate]); + } + } + if (errorOccurred) { + if ([httpConnectionDelegate abortRequestedForHTTPConnection:self]) { + [_error release]; + _error = [[NSError alloc] initWithDomain:[self errorDomain] code:ERROR_ABORT_REQUESTED userInfo:[NSDictionary dictionaryWithObject:@"abort requested" forKey:NSLocalizedDescriptionKey]]; + } + if (error != NULL) { + *error = _error; + } + return nil; + } + hasBytesAvailable = NO; + id ret = nil; + if (complete) { + HSLogTrace(@"%@: empty response body", self); + ret = [[NSData data] newInputStream]; + } else { + NSAssert(responseHeaders != nil, @"responseHeaders can't be nil"); + + NSString *transferEncoding = [self responseHeaderForKey:@"Transfer-Encoding"]; + // NSString *contentLength = [self responseHeaderForKey:@"Content-Length"]; + HSLogTrace(@"Content-Length = %@", [self responseHeaderForKey:@"Content-Length"]); + HSLogTrace(@"Transfer-Encoding = %@", transferEncoding); + if (transferEncoding != nil && ![transferEncoding isEqualToString:@"Identity"]) { + if ([[transferEncoding lowercaseString] isEqualToString:@"chunked"]) { + HSLogTrace(@"%@: chunked response body", self); + ret = [[ChunkedInputStream alloc] initWithUnderlyingStream:self]; + } else { + SETNSERROR(@"StreamErrorDomain", -1, @"unknown Transfer-Encoding '%@'", transferEncoding); + return nil; + } + // } else if (contentLength != nil) { + // int length = [contentLength intValue]; + // BufferedInputStream *bis = [[BufferedInputStream alloc] initWithUnderlyingStream:self]; + // HSLogTrace(@"%@: fixed-length response body (%d bytes)", self, length); + //// ret = [[FixedLengthInputStream alloc] initWithUnderlyingStream:bis length:(NSUInteger)length]; + //// [bis release]; + // ret = bis; + } else { + /* + * FIXME: handle multipart/byteranges media type. + * See rfc2616 section 4.4 ("message length"). + */ + HSLogTrace(@"%@: response body with no content-length", self); + ret = [self retain]; + } + } + return ret; +} +- (NSInteger)doRead:(unsigned char *)buf bufferLength:(NSUInteger)length error:(NSError **)error { + NSInteger recvd = 0; + for (;;) { + NSTimeInterval timeoutSeconds = [httpTimeoutSetting timeoutSeconds]; + if (timeoutSeconds == 0) { + timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; + } + NSDate *timeout = [[NSDate date] addTimeInterval:timeoutSeconds]; + while (!complete && !hasBytesAvailable) { + if ([timeout earlierDate:[NSDate date]] == timeout) { + HSLogWarn(@"timed out after %0.2f seconds waiting for response data from %@ %@", timeoutSeconds, requestMethod, url); + SETNSERROR([self errorDomain], ERROR_TIMEOUT, @"timeout after %0.2f seconds", timeoutSeconds); + closeRequested = YES; + return -1; + } + HSLogTrace(@"read: running the runloop until %@", timeout); + [[NSRunLoop currentRunLoop] runMode:runLoopMode beforeDate:timeout]; + } + hasBytesAvailable = NO; + if (errorOccurred) { + if (error != NULL) { + *error = _error; + } + return -1; + } + if (complete) { + return 0; + } + recvd = [readStream read:buf maxLength:length]; + HSLogTrace(@"received %d bytes", recvd); + if (recvd < 0) { + [self handleStreamError]; + if (error != NULL) { + *error = _error; + } + return -1; + } + if (recvd > 0) { + break; + } + } + return recvd; +} +@end + +@implementation CFHTTPConnection (callback) +- (void)sentRequestBytes:(NSInteger)count { + totalSent += count; +} +@end diff --git a/http/CFHTTPInputStream.h b/http/CFHTTPInputStream.h new file mode 100644 index 0000000..4d2fea4 --- /dev/null +++ b/http/CFHTTPInputStream.h @@ -0,0 +1,28 @@ +// +// CFHTTPInputStream.h +// Arq +// +// Created by Stefan Reitshamer on 3/16/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import +@class CFHTTPConnection; +@protocol HTTPConnectionDelegate; +@class NetMonitor; + + +@interface CFHTTPInputStream : NSObject { + CFHTTPConnection *conn; + NSInputStream *inputStream; + id httpConnectionDelegate; + int throttleType; + int throttleKBPS; + NSTimeInterval lastReceivedTime; + NSUInteger lastReceivedLength; + NSUInteger totalReceivedLength; + NetMonitor *netMonitor; +} +- (id)initWithCFHTTPConnection:(CFHTTPConnection *)theConn data:(NSData *)theData httpConnectionDelegate:(id )theHTTPConnectionDelegate; + +@end diff --git a/http/CFHTTPInputStream.m b/http/CFHTTPInputStream.m new file mode 100644 index 0000000..13710af --- /dev/null +++ b/http/CFHTTPInputStream.m @@ -0,0 +1,135 @@ +// +// CFHTTPInputStream.m +// Arq +// +// Created by Stefan Reitshamer on 3/16/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import "CFHTTPInputStream.h" +#import "HTTPConnectionDelegate.h" +#import "CFHTTPConnection.h" +#import "NetMonitor.h" + +@interface CFHTTPConnection (callback) +- (void)sentRequestBytes:(NSInteger)count; +@end + +@implementation CFHTTPInputStream +- (id)initWithCFHTTPConnection:(CFHTTPConnection *)theConn data:(NSData *)theData httpConnectionDelegate:(id )theHTTPConnectionDelegate { + if (self = [super init]) { + conn = theConn; // Don't retain the connection. + inputStream = [[NSInputStream inputStreamWithData:theData] retain]; + httpConnectionDelegate = theHTTPConnectionDelegate; + netMonitor = [[NetMonitor alloc] init]; + } + return self; +} +- (void)dealloc { + [inputStream release]; + [netMonitor release]; + [super dealloc]; +} + +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate]; + if (throttleType == THROTTLE_FIXED && throttleKBPS != 0) { + // Don't send more than 1/10th of the max bytes/sec: + NSUInteger maxLen = throttleKBPS * 100; + if (len > maxLen) { + len = maxLen; + } + + if (lastReceivedTime != 0) { + NSTimeInterval interval = currentTime - lastReceivedTime; + + // For some reason Activity Monitor reports "Data sent/sec" at twice what we seem to be sending! + // So we send half as much -- we divide by 500 instead of 1000 here: + NSTimeInterval throttledInterval = (double)lastReceivedLength / ((double)throttleKBPS * (double)500.0); + + if (throttledInterval > interval) { + [NSThread sleepForTimeInterval:(throttledInterval - interval)]; + } + } + } + + if (throttleType == THROTTLE_AUTOMATIC) { + NSTimeInterval interval = currentTime - lastReceivedTime; + if (lastReceivedLength > 0) { + double myBPS = (double)lastReceivedLength / interval; + double throttle = [netMonitor sample:myBPS]; + if (throttle < 1.0) { + HSLogDebug(@"throttle = %f", throttle); + } + NSTimeInterval throttledInterval = (throttle == 0) ? 0.5 : ((interval / throttle) - interval); + if (throttledInterval > 0) { + if (throttledInterval > 0.5) { + throttledInterval = 0.5; + } + HSLogDebug(@"auto-throttle: sleeping %f seconds", throttledInterval); + [NSThread sleepForTimeInterval:throttledInterval]; + } + } + } + + NSInteger ret = [inputStream read:buffer maxLength:len]; + if (ret >= 0) { + if ([httpConnectionDelegate respondsToSelector:@selector(httpConnection:sentBytes:throttleType:throttleKBPS:pauseRequested:abortRequested:)]) { + BOOL pauseRequested = NO; + BOOL abortRequested = NO; + [httpConnectionDelegate httpConnection:conn sentBytes:ret throttleType:&throttleType throttleKBPS:&throttleKBPS pauseRequested:&pauseRequested abortRequested:&abortRequested]; + if (pauseRequested || abortRequested) { + return -1; + } + } + lastReceivedTime = currentTime; + lastReceivedLength = ret; + totalReceivedLength += ret; + } + [conn sentRequestBytes:ret]; + return ret; +} + + +// Implement most of the NSInputStream methods: +- (void)open { + [inputStream open]; +} +- (void)close { + [inputStream close]; +} +- (id)delegate { + return [inputStream delegate]; +} +- (void)setDelegate:(id)theDelegate { + [inputStream setDelegate:theDelegate]; +} +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [inputStream scheduleInRunLoop:aRunLoop forMode:mode]; +} +- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [inputStream removeFromRunLoop:aRunLoop forMode:mode]; +} +- (id)propertyForKey:(NSString *)key { + return [inputStream propertyForKey:key]; +} +- (BOOL)setProperty:(id)property forKey:(NSString *)key { + return [inputStream setProperty:property forKey:key]; +} +- (NSStreamStatus)streamStatus { + return [inputStream streamStatus]; +} +- (NSError *)streamError { + return [inputStream streamError]; +} + + +// Forward everything else to the inputStream ivar. +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + return [inputStream methodSignatureForSelector:aSelector]; +} +- (void)forwardInvocation:(NSInvocation *)anInvocation { + [anInvocation invokeWithTarget:inputStream]; +} + +@end diff --git a/http/CFNetwork.h b/http/CFNetwork.h new file mode 100644 index 0000000..f46204f --- /dev/null +++ b/http/CFNetwork.h @@ -0,0 +1,41 @@ +/* + Copyright (c) 2009-2010, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + + +@interface CFNetwork : NSObject { + +} ++ (NSString *)errorDomain; ++ (NSError *)NSErrorWithNetworkError:(CFErrorRef)err; +@end diff --git a/http/CFNetwork.m b/http/CFNetwork.m new file mode 100644 index 0000000..851d6fe --- /dev/null +++ b/http/CFNetwork.m @@ -0,0 +1,130 @@ +/* + Copyright (c) 2009-2010, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#import "CFNetwork.h" +#import "DNS_SDErrors.h" + +@implementation CFNetwork ++ (NSString *)errorDomain { + return @"CFNetworkErrorDomain"; +} ++ (NSError *)NSErrorWithNetworkError:(CFErrorRef)err { + NSString *localizedDescription = @"Network error"; + NSString *domain = (NSString *)CFErrorGetDomain(err); + CFIndex code = CFErrorGetCode(err); + CFDictionaryRef userInfo = CFErrorCopyUserInfo(err); + if ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork]) { + if (code == kCFHostErrorHostNotFound) { + localizedDescription = @"host not found"; + } else if (code == kCFHostErrorUnknown) { + int gaiCode = 0; + if (CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(userInfo, kCFGetAddrInfoFailureKey), kCFNumberIntType, &gaiCode)) { + HSLogDebug(@"Host lookup error: %s", gai_strerror(gaiCode)); + localizedDescription = @"Could not connect to the Internet"; + } + } else if (code == kCFSOCKSErrorUnknownClientVersion) { + localizedDescription = @"Unknown SOCKS client version"; + } else if (code == kCFSOCKSErrorUnsupportedServerVersion) { + localizedDescription = [NSString stringWithFormat:@"Unsupported SOCKS server version (server requested version %@)", (NSString *)CFDictionaryGetValue(userInfo, kCFSOCKSVersionKey)]; + } else if (code == kCFSOCKS4ErrorRequestFailed) { + localizedDescription = @"SOCKS4 request rejected or failed"; + } else if (code == kCFSOCKS4ErrorIdentdFailed) { + localizedDescription = @"SOCKS4 server cannot connect to identd on the client"; + } else if (code == kCFSOCKS4ErrorIdConflict) { + localizedDescription = @"SOCKS4 client and identd report different user IDs"; + } else if (code == kCFSOCKS4ErrorUnknownStatusCode) { + localizedDescription = [NSString stringWithFormat:@"SOCKS4 error %@", (NSString *)CFDictionaryGetValue(userInfo, kCFSOCKSStatusCodeKey)]; + } else if (code == kCFSOCKS5ErrorBadState) { + localizedDescription = @"SOCKS5 bad state"; + } else if (code == kCFSOCKS5ErrorBadResponseAddr) { + localizedDescription = @"SOCKS5 bad credentials"; + } else if (code == kCFSOCKS5ErrorBadCredentials) { + localizedDescription = @"SOCKS5 unsupported negotiation method"; + } else if (code == kCFSOCKS5ErrorUnsupportedNegotiationMethod) { + localizedDescription = @"SOCKS5 unsupported negotiation method"; + } else if (code == kCFSOCKS5ErrorNoAcceptableMethod) { + localizedDescription = @"SOCKS5 no acceptable method"; + } else if (code == kCFNetServiceErrorUnknown) { + localizedDescription = @"Unknown Net Services error"; + } else if (code == kCFNetServiceErrorCollision) { + localizedDescription = @"Net Services: collision"; + } else if (code == kCFNetServiceErrorNotFound) { + localizedDescription = @"Net Services: not found"; + } else if (code == kCFNetServiceErrorInProgress) { + localizedDescription = @"Net Services: in progress"; + } else if (code == kCFNetServiceErrorBadArgument) { + localizedDescription = @"Net Services: bad argument"; + } else if (code == kCFNetServiceErrorCancel) { + localizedDescription = @"Net Services: cancelled"; + } else if (code == kCFNetServiceErrorInvalid) { + localizedDescription = @"Net Services: invalid"; + } else if (code == kCFNetServiceErrorTimeout) { + localizedDescription = @"Net Services timeout"; + } else if (code == kCFNetServiceErrorDNSServiceFailure) { + localizedDescription = @"Net Services DNS failure"; + int dns_sdCode = 0; + if (CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(userInfo, kCFDNSServiceFailureKey), kCFNumberIntType, &dns_sdCode)) { + localizedDescription = [NSString stringWithFormat:@"Net Services DNS failure: %@", [DNS_SDErrors descriptionForDNS_SDError:dns_sdCode]]; + } + } else if (code == kCFFTPErrorUnexpectedStatusCode) { + localizedDescription = [NSString stringWithFormat:@"FTP error %@", (NSString *)CFDictionaryGetValue(userInfo, kCFFTPStatusCodeKey)]; + } else if (code == kCFErrorHTTPAuthenticationTypeUnsupported) { + localizedDescription = @"HTTP authentication type unsupported"; + } else if (code == kCFErrorHTTPBadCredentials) { + localizedDescription = @"bad HTTP credentials"; + } else if (code == kCFErrorHTTPConnectionLost) { + localizedDescription = @"HTTP connection lost"; + } else if (code == kCFErrorHTTPParseFailure) { + localizedDescription = @"HTTP parse failure"; + } else if (code == kCFErrorHTTPRedirectionLoopDetected) { + localizedDescription = @"HTTP redirection loop detected"; + } else if (code == kCFErrorHTTPBadURL) { + localizedDescription = @"bad HTTP URL"; + } else if (code == kCFErrorHTTPProxyConnectionFailure) { + localizedDescription = @"HTTP proxy connection failure"; + } else if (code == kCFErrorHTTPBadProxyCredentials) { + localizedDescription = @"bad HTTP proxy credentials"; + } else if (code == kCFErrorPACFileError) { + localizedDescription = @"HTTP PAC file error"; + } + } else if ([domain isEqualToString:@"NSPOSIXErrorDomain"] && code == ENOTCONN) { + localizedDescription = @"Lost connection to the Internet"; + } else { + localizedDescription = [(NSString *)CFErrorCopyDescription(err) autorelease]; + } + CFRelease(userInfo); + return [NSError errorWithDomain:[CFNetwork errorDomain] code:code userInfo:[NSDictionary dictionaryWithObjectsAndKeys:localizedDescription, NSLocalizedDescriptionKey, nil]]; +} +@end diff --git a/http/HTTPConnectionDelegate.h b/http/HTTPConnectionDelegate.h new file mode 100644 index 0000000..08542e6 --- /dev/null +++ b/http/HTTPConnectionDelegate.h @@ -0,0 +1,19 @@ +// +// HTTPConnectionDelegate.h +// Arq +// +// Created by Stefan Reitshamer on 3/16/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +@protocol HTTPConnection; + +#define THROTTLE_NONE 0 +#define THROTTLE_AUTOMATIC 1 +#define THROTTLE_FIXED 2 + +@protocol HTTPConnectionDelegate +- (void)httpConnection:(id )theHTTPConnection sentBytes:(unsigned long long)sent throttleType:(int *)theThrottleType throttleKBPS:(int *)theThrottleKBPS pauseRequested:(BOOL *)isPauseRequested abortRequested:(BOOL *)isAbortRequested; +- (void)httpConnection:(id )theHTTPConnection subtractSentBytes:(unsigned long long)sent; +- (BOOL)abortRequestedForHTTPConnection:(id )theHTTPConnection; +@end diff --git a/http/HTTPConnectionFactory.h b/http/HTTPConnectionFactory.h new file mode 100644 index 0000000..27bf11c --- /dev/null +++ b/http/HTTPConnectionFactory.h @@ -0,0 +1,27 @@ +// +// HTTPConnectionFactory.h +// Arq +// +// Created by Stefan Reitshamer on 3/15/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import +@protocol HTTPConnection; +@protocol HTTPConnectionDelegate; +@class HTTPTimeoutSetting; + + +@interface HTTPConnectionFactory : NSObject { + NSTimeInterval maxConnectionLifetime; + NSLock *lock; + NSMutableDictionary *connectionMapsByThreadId; +} + ++ (HTTPConnectionFactory *)theFactory; +- (id )newHTTPConnectionToURL:(NSURL *)theURL + method:(NSString *)theMethod + httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting + httpConnectionDelegate:(id )theHTTPConnectionDelegate; + +@end diff --git a/http/HTTPConnectionFactory.m b/http/HTTPConnectionFactory.m new file mode 100644 index 0000000..23eed93 --- /dev/null +++ b/http/HTTPConnectionFactory.m @@ -0,0 +1,163 @@ +// +// HTTPConnectionFactory.m +// Arq +// +// Created by Stefan Reitshamer on 3/15/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import "HTTPConnectionFactory.h" +#import "CFHTTPConnection.h" +#import "URLConnection.h" + +#define DEFAULT_MAX_HTTPCONNECTION_LIFETIME_SECONDS (20) +#define CLEANUP_THREAD_SLEEP_SECONDS (5) + + +@interface ConnectionMap : NSObject { + NSMutableDictionary *connections; +} +- (CFHTTPConnection *)newConnectionToURL:(NSURL *)theURL + method:(NSString *)theMethod + maxConnectionLifetime:(NSTimeInterval)theMaxConnectionLifetime + httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting + httpConnectionDelegate:(id )theDelegate; +- (void)dropUnusableConnections:(NSTimeInterval)theMaxConnectionLifetime; +@end + +@implementation ConnectionMap +- (id)init { + if (self = [super init]) { + connections = [[NSMutableDictionary alloc] init]; + } + return self; +} +- (void)dealloc { + [connections release]; + [super dealloc]; +} +- (CFHTTPConnection *)newConnectionToURL:(NSURL *)theURL + method:(NSString *)theMethod + maxConnectionLifetime:(NSTimeInterval)theMaxConnectionLifetime + httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting + httpConnectionDelegate:(id )theHTTPConnectionDelegate { + NSString *key = [NSString stringWithFormat:@"%@ %@://%@:%d", theMethod, [theURL scheme], [theURL host], [[theURL port] intValue]]; + CFHTTPConnection *conn = [connections objectForKey:key]; + if (conn != nil) { + if ([conn isCloseRequested] || (([NSDate timeIntervalSinceReferenceDate] - [conn createTime]) > theMaxConnectionLifetime)) { + [connections removeObjectForKey:key]; + HSLogTrace(@"removing connection %p", conn); + conn = nil; + } else { + HSLogTrace(@"reusing connection %p", conn); + conn = [[CFHTTPConnection alloc] initWithURL:theURL method:theMethod httpTimeoutSetting:theHTTPTimeoutSetting httpConnectionDelegate:theHTTPConnectionDelegate previousConnection:conn]; + [connections setObject:conn forKey:key]; + } + } + if (conn == nil) { + HSLogTrace(@"new connection %p", conn); + conn = [[CFHTTPConnection alloc] initWithURL:theURL method:theMethod httpTimeoutSetting:theHTTPTimeoutSetting httpConnectionDelegate:theHTTPConnectionDelegate]; +// [connections setObject:conn forKey:key]; + } + return conn; +} + +- (void)dropUnusableConnections:(NSTimeInterval)theMaxConnectionLifetime { + NSMutableArray *keysToDrop = [NSMutableArray array]; + for (NSString *key in [connections allKeys]) { + CFHTTPConnection *conn = [connections objectForKey:key]; + if ([conn isCloseRequested] || (([NSDate timeIntervalSinceReferenceDate] - [conn createTime]) > theMaxConnectionLifetime)) { // FIXME: Duplicate logic to newConnectionToURL: method + [keysToDrop addObject:key]; + } + } + if ([keysToDrop count] > 0) { + HSLogTrace(@"dropping %@", keysToDrop); + [connections removeObjectsForKeys:keysToDrop]; + } +} +@end + +static HTTPConnectionFactory *theFactory = nil; + +@implementation HTTPConnectionFactory ++ (HTTPConnectionFactory *)theFactory { + if (theFactory == nil) { + theFactory = [[super allocWithZone:NULL] init]; + } + return theFactory; +} + +/* Singleton recipe: */ ++ (id)allocWithZone:(NSZone *)zone { + return [[HTTPConnectionFactory theFactory] retain]; +} +- (id)copyWithZone:(NSZone *)zone { + return self; +} +- (id)retain { + return self; +} +- (NSUInteger)retainCount { + return NSUIntegerMax; //denotes an object that cannot be released +} +- (void)release { + //do nothing +} +- (id)autorelease { + return self; +} + +- (id)init { + if (self = [super init]) { + lock = [[NSLock alloc] init]; + [lock setName:@"HTTPConnectionFactory lock"]; + connectionMapsByThreadId = [[NSMutableDictionary alloc] init]; + maxConnectionLifetime = DEFAULT_MAX_HTTPCONNECTION_LIFETIME_SECONDS; + [NSThread detachNewThreadSelector:@selector(dropUnusableConnections) toTarget:self withObject:nil]; + } + return self; +} +- (void)dealloc { + [lock release]; + [connectionMapsByThreadId release]; + [super dealloc]; +} +- (id )newHTTPConnectionToURL:(NSURL *)theURL method:(NSString *)theMethod httpTimeoutSetting:(HTTPTimeoutSetting *)theHTTPTimeoutSetting httpConnectionDelegate:(id )theHTTPConnectionDelegate { + id ret = nil; + void *pthreadPtr = pthread_self(); +#ifdef __LP64__ + NSNumber *threadID = [NSNumber numberWithUnsignedLongLong:(uint64_t)pthreadPtr]; +#else + NSNumber *threadID = [NSNumber numberWithUnsignedLong:(uint32_t)pthreadPtr]; +#endif + [lock lock]; + ConnectionMap *connMap = [connectionMapsByThreadId objectForKey:threadID]; + if (connMap == nil) { + connMap = [[ConnectionMap alloc] init]; + [connectionMapsByThreadId setObject:connMap forKey:threadID]; + [connMap release]; + } + ret = [connMap newConnectionToURL:theURL method:theMethod maxConnectionLifetime:maxConnectionLifetime httpTimeoutSetting:theHTTPTimeoutSetting httpConnectionDelegate:theHTTPConnectionDelegate]; + [lock unlock]; + return ret; +} + +#pragma mark cleanup thread +- (void)dropUnusableConnections { + [self retain]; + for (;;) { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + [NSThread sleepForTimeInterval:CLEANUP_THREAD_SLEEP_SECONDS]; + [lock lock]; + for (ConnectionMap *connMap in [connectionMapsByThreadId allValues]) { + @try { + [connMap dropUnusableConnections:maxConnectionLifetime]; + } @catch(NSException *e) { + HSLogError(@"unexpected exception in HTTPConnectionFactory cleanup thread: %@", [e description]); + } + } + [lock unlock]; + [pool drain]; + } +} +@end diff --git a/http/HTTPTimeoutSetting.h b/http/HTTPTimeoutSetting.h new file mode 100644 index 0000000..9e067a9 --- /dev/null +++ b/http/HTTPTimeoutSetting.h @@ -0,0 +1,16 @@ +// +// HTTPTimeoutSetting.h +// Arq +// +// Created by Stefan Reitshamer on 4/6/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + + +@interface HTTPTimeoutSetting : NSObject { + NSTimeInterval timeoutSeconds; +} +- (id)init; +- (id)initWithTimeoutSeconds:(NSTimeInterval)theTimeoutSeconds; +- (NSTimeInterval)timeoutSeconds; +@end diff --git a/http/HTTPTimeoutSetting.m b/http/HTTPTimeoutSetting.m new file mode 100644 index 0000000..24c42dd --- /dev/null +++ b/http/HTTPTimeoutSetting.m @@ -0,0 +1,35 @@ +// +// HTTPTimeoutSetting.m +// Arq +// +// Created by Stefan Reitshamer on 4/6/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import "HTTPTimeoutSetting.h" + +#define DEFAULT_TIMEOUT_SECONDS 30.0 + +@implementation HTTPTimeoutSetting +- (id)init { + self = [super init]; + return self; +} +- (id)initWithTimeoutSeconds:(NSTimeInterval)theTimeoutSeconds { + if (self = [super init]) { + timeoutSeconds = theTimeoutSeconds; + } + return self; +} +- (NSTimeInterval)timeoutSeconds { + NSTimeInterval ret = timeoutSeconds; + if (ret == 0) { + [[NSUserDefaults standardUserDefaults] synchronize]; + ret = (NSTimeInterval)[[NSUserDefaults standardUserDefaults] doubleForKey:@"HTTPTimeoutSeconds"]; + } + if (ret == 0) { + ret = DEFAULT_TIMEOUT_SECONDS; + } + return ret; +} +@end diff --git a/io/NetMonitor.h b/io/NetMonitor.h new file mode 100644 index 0000000..d5c3ee9 --- /dev/null +++ b/io/NetMonitor.h @@ -0,0 +1,48 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +#import +@class NetMonSample; + +@interface NetMonitor : NSObject { + NetMonSample *currentSample; + NetMonSample *previousSample; + NSTimeInterval overThresholdTime; + NSUInteger numBPSSamples; + double bpsSamples[4]; +} +- (id)init; +- (double)sample:(double)myBPS; +- (double)averageBPS; +@end diff --git a/io/NetMonitor.m b/io/NetMonitor.m new file mode 100644 index 0000000..69e5b93 --- /dev/null +++ b/io/NetMonitor.m @@ -0,0 +1,132 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +#import "NetMonitor.h" +#import "Sysctl.h" + +#define OVERHEAD_IN_BYTES (300) +#define THROTTLE_UP_SECONDS (8.0) +#define THRESHOLD_RATIO (.3) + +@interface NetMonSample : NSObject { + NSTimeInterval timeInterval; + unsigned long long rawBytesIn; +} +@property NSTimeInterval timeInterval; +@property unsigned long long rawBytesIn; +@end +@implementation NetMonSample +@synthesize timeInterval, rawBytesIn; +@end + +@implementation NetMonitor +- (id)init { + if (self = [super init]) { + currentSample = [[NetMonSample alloc] init]; + previousSample = [[NetMonSample alloc] init]; + NSError *error = nil; + unsigned long long rawinb; + unsigned long long rawoutb; + if (![Sysctl networkBytesIn:&rawinb bytesOut:&rawoutb error:&error]) { + HSLogError(@"failed to get net stats: %@", [error localizedDescription]); + [self release]; + return nil; + } + currentSample.timeInterval = [NSDate timeIntervalSinceReferenceDate]; + currentSample.rawBytesIn = rawinb; + overThresholdTime = [[NSDate distantPast] timeIntervalSinceReferenceDate]; + memset(bpsSamples, 0, sizeof(bpsSamples)); + } + return self; +} +- (void)dealloc { + [currentSample release]; + [previousSample release]; + [super dealloc]; +} +- (double)sample:(double)myBPS { + bpsSamples[(numBPSSamples++ % 4)] = myBPS; + double throttle = 1.0; + if (numBPSSamples > 1) { + unsigned long long rawinb; + unsigned long long rawoutb; + NSError *error = nil; + if (![Sysctl networkBytesIn:&rawinb bytesOut:&rawoutb error:&error]) { + HSLogError(@"failed to get net stats: %@", [error localizedDescription]); + } else { + previousSample.timeInterval = currentSample.timeInterval; + previousSample.rawBytesIn = currentSample.rawBytesIn; + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; + currentSample.timeInterval = now; + currentSample.rawBytesIn = rawinb; + + // I got a crash once because rawinb was smaller than previousSample.rawBytesIn, so now we check: + if (rawinb >= previousSample.rawBytesIn) { + if (previousSample.timeInterval != 0) { + unsigned long long bytesIn = currentSample.rawBytesIn - previousSample.rawBytesIn; + if (bytesIn > OVERHEAD_IN_BYTES) { + bytesIn -= OVERHEAD_IN_BYTES; + } + NSTimeInterval interval = currentSample.timeInterval - previousSample.timeInterval; + double inBPS = (double)bytesIn / interval; + double ratio = myBPS == 0 ? 0 : (inBPS / myBPS); + BOOL overThreshold = myBPS != 0 && ratio > THRESHOLD_RATIO; + HSLogTrace(@"sent: %0.2fBPS bytesIn: %qu (%0.2fBPS) ratio: %0.2f threshold: %0.2f interval: %0.2f %@", myBPS, bytesIn, inBPS, ratio, THRESHOLD_RATIO, interval, (overThreshold ? @": over threshold" : @"")); + if (overThreshold) { + overThresholdTime = [NSDate timeIntervalSinceReferenceDate]; + } + } + } + + double secsSinceOverage = now - overThresholdTime; + if (secsSinceOverage < THROTTLE_UP_SECONDS) { + throttle = secsSinceOverage / THROTTLE_UP_SECONDS; + if (throttle < 0.0001) { + throttle = 0.0001; + } + HSLogTrace(@"secsSinceOverage=%f throttle=%0.2f", secsSinceOverage, throttle); + } + } + } + return throttle; +} +- (double)averageBPS { + NSUInteger num = numBPSSamples > 4 ? 4 : numBPSSamples; + double total = 0.0; + for (NSUInteger i = 0; i < num; i++) { + total += bpsSamples[i]; + } + return total / (double)num; +} +@end diff --git a/s3/RemoteS3Signer.h b/s3/RemoteS3Signer.h new file mode 100644 index 0000000..5f7d1f4 --- /dev/null +++ b/s3/RemoteS3Signer.h @@ -0,0 +1,20 @@ +// +// RemoteS3Signer.h +// Arq +// +// Created by Stefan Reitshamer on 12/30/10. +// Copyright 2010 __MyCompanyName__. All rights reserved. +// + +#import +#import "S3Signer.h" + +@interface RemoteS3Signer : NSObject { + NSString *accessKey; + NSURL *url; + NSString *account; + NSString *password; +} ++ (NSString *)errorDomain; +- (id)initWithAccessKey:(NSString *)theAccessKey url:(NSURL *)theURL account:(NSString *)theAccount password:(NSString *)thePassword; +@end diff --git a/s3/RemoteS3Signer.m b/s3/RemoteS3Signer.m new file mode 100644 index 0000000..e30258e --- /dev/null +++ b/s3/RemoteS3Signer.m @@ -0,0 +1,75 @@ +// +// RemoteS3Signer.m +// Arq +// +// Created by Stefan Reitshamer on 12/30/10. +// Copyright 2010 __MyCompanyName__. All rights reserved. +// + +#import "RemoteS3Signer.h" +#import "HTTPConnection.h" +#import "URLConnection.h" +#import "NSData-InputStream.h" +#import "SetNSError.h" +#import "HTTP.h" +#import "InputStream.h" + + +@implementation RemoteS3Signer ++ (NSString *)errorDomain { + return @"RemoteS3SignerErrorDomain"; +} +- (id)initWithAccessKey:(NSString *)theAccessKey url:(NSURL *)theURL account:(NSString *)theAccount password:(NSString *)thePassword { + if (self = [super init]) { + accessKey = [theAccessKey retain]; + url = [theURL retain]; + account = [theAccount retain]; + password = [thePassword retain]; + } + return self; +} +- (void)dealloc { + [accessKey release]; + [url release]; + [account release]; + [password release]; + [super dealloc]; +} + + +#pragma mark NSCopying +- (id)copyWithZone:(NSZone *)zone { + return [[RemoteS3Signer alloc] initWithAccessKey:accessKey url:url account:account password:password]; +} + + +#pragma mark S3Signer +- (NSString *)sign:(NSString *)theString error:(NSError **)error { + id conn = [[[URLConnection alloc] initWithURL:url method:@"POST" delegate:nil] autorelease]; + [conn setRequestHeader:accessKey forKey:@"X-Arq-AccessKey"]; + NSData *requestData = [theString dataUsingEncoding:NSUTF8StringEncoding]; + if (![conn executeRequestWithBody:requestData error:error]) { + return nil; + } + id responseBodyStream = [conn newResponseBodyStream:error]; + if (responseBodyStream == nil) { + return nil; + } + NSData *data = [responseBodyStream slurp:error]; + [responseBodyStream release]; + if (data == nil) { + return nil; + } + int code = [conn responseCode]; + if (code != HTTP_OK) { + SETNSERROR([RemoteS3Signer errorDomain], -1, @"unexpected HTTP status code %d", code); + return nil; + } + NSString *sig = [[[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding] autorelease]; + while ([sig hasSuffix:@"\n"]) { + sig = [sig substringToIndex:([sig length] - 1)]; + } + HSLogTrace(@"signature: %@", sig); + return sig; +} +@end diff --git a/s3/S3AuthorizationProvider.h b/s3/S3AuthorizationProvider.h index 8eefea3..1aed827 100644 --- a/s3/S3AuthorizationProvider.h +++ b/s3/S3AuthorizationProvider.h @@ -34,11 +34,13 @@ @protocol S3Signer; @protocol HTTPConnection; -@interface S3AuthorizationProvider : NSObject { +@interface S3AuthorizationProvider : NSObject { NSString *accessKey; id signer; } - (id)initWithAccessKey:(NSString *)access secretKey:(NSString *)secret; +- (id)initWithAccessKey:(NSString *)access url:(NSURL *)theURL account:(NSString *)theAccount password:(NSString *)thePassword; +- (id)initWithAccessKey:(NSString *)access signer:(id )theSigner; - (NSString *)accessKey; -- (BOOL)setAuthorizationRequestHeaderOnHTTPConnection:(id )conn usingS3BucketName:(NSString *)s3BucketName error:(NSError **)error; +- (BOOL)setAuthorizationRequestHeaderOnHTTPConnection:(id )conn error:(NSError **)error; @end diff --git a/s3/S3AuthorizationProvider.m b/s3/S3AuthorizationProvider.m index eeab175..f3644b6 100644 --- a/s3/S3AuthorizationProvider.m +++ b/s3/S3AuthorizationProvider.m @@ -32,6 +32,7 @@ #import "S3AuthorizationProvider.h" #import "LocalS3Signer.h" +#import "RemoteS3Signer.h" #import "HTTPConnection.h" /* @@ -41,7 +42,7 @@ @interface S3AuthorizationProvider (internal) - (NSString *)authorizationForString:(NSString *)stringToSign error:(NSError **)error; -- (NSString *)stringToSignForS3BucketName:(NSString *)theS3BucketName connection:(id )theConnection; +- (NSString *)stringToSignForConnection:(id )theConnection; @end @implementation S3AuthorizationProvider @@ -54,6 +55,20 @@ } return self; } +- (id)initWithAccessKey:(NSString *)access url:(NSURL *)theURL account:(NSString *)theAccount password:(NSString *)thePassword { + if (self = [super init]) { + accessKey = [access copy]; + signer = [[RemoteS3Signer alloc] initWithAccessKey:accessKey url:theURL account:theAccount password:thePassword]; + } + return self; +} +- (id)initWithAccessKey:(NSString *)access signer:(id )theSigner { + if (self = [super init]) { + accessKey = [access retain]; + signer = [theSigner retain]; + } + return self; +} - (void)dealloc { [accessKey release]; [signer release]; @@ -62,8 +77,8 @@ - (NSString *)accessKey { return accessKey; } -- (BOOL)setAuthorizationRequestHeaderOnHTTPConnection:(id )conn usingS3BucketName:(NSString *)s3BucketName error:(NSError **)error { - NSString *stringToSign = [self stringToSignForS3BucketName:s3BucketName connection:conn]; +- (BOOL)setAuthorizationRequestHeaderOnHTTPConnection:(id )conn error:(NSError **)error { + NSString *stringToSign = [self stringToSignForConnection:conn]; NSString *authorization = [self authorizationForString:stringToSign error:error]; if (authorization == nil) { return NO; @@ -71,6 +86,12 @@ [conn setRequestHeader:authorization forKey:@"Authorization"]; return YES; } + + +#pragma mark NSCopying +- (id)copyWithZone:(NSZone *)zone { + return [[S3AuthorizationProvider alloc] initWithAccessKey:accessKey signer:signer]; +} @end @implementation S3AuthorizationProvider (internal) @@ -91,7 +112,7 @@ } return ret; } -- (NSString *)stringToSignForS3BucketName:(NSString *)theS3BucketName connection:(id )theConnection { +- (NSString *)stringToSignForConnection:(id )theConnection { NSMutableString *buf = [[[NSMutableString alloc] init] autorelease]; [buf appendString:[theConnection requestMethod]]; [buf appendString:@"\n"]; @@ -118,10 +139,6 @@ for (NSString *xamz in xamzHeaders) { [buf appendString:xamz]; } - if ([theS3BucketName length] > 0) { - [buf appendString:@"/"]; - [buf appendString:theS3BucketName]; - } [buf appendString:[theConnection requestPathInfo]]; NSString *queryString = [theConnection requestQueryString]; if ([queryString isEqualToString:@"?acl"] diff --git a/s3/S3Lister.m b/s3/S3Lister.m index 616bf04..0167935 100644 --- a/s3/S3Lister.m +++ b/s3/S3Lister.m @@ -125,7 +125,10 @@ [queryString appendString:[NSString stringWithFormat:@"&marker=%@", [suffix stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; } - S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:[NSString stringWithFormat:@"/%@/", s3BucketName] queryString:queryString authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError]; + S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:[NSString stringWithFormat:@"/%@/", s3BucketName] queryString:queryString authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError error:error]; + if (s3r == nil) { + return NO; + } ServerBlob *sb = [s3r newServerBlob:error]; [s3r release]; if (sb == nil) { @@ -136,8 +139,10 @@ if (data == nil) { return NO; } - NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData:data options:0 error:error] autorelease]; + NSError *myError = nil; + NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData:data options:0 error:&myError] autorelease]; if (!xmlDoc) { + SETNSERROR([S3Service errorDomain], [myError code], @"error parsing List Objects XML response: %@", myError); return NO; } NSString *lastPath = nil; diff --git a/s3/S3Owner.h b/s3/S3Owner.h new file mode 100644 index 0000000..d4a1410 --- /dev/null +++ b/s3/S3Owner.h @@ -0,0 +1,43 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + + +@interface S3Owner : NSObject { + NSString *displayName; + NSString *idString; +} +- (id)initWithDisplayName:(NSString *)dn idString:(NSString *)ids; +- (NSString *)displayName; +- (NSString *)idString; +@end diff --git a/s3/S3Owner.m b/s3/S3Owner.m new file mode 100644 index 0000000..952fdc7 --- /dev/null +++ b/s3/S3Owner.m @@ -0,0 +1,50 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "S3Owner.h" + + +@implementation S3Owner +- (id)initWithDisplayName:(NSString *)dn idString:(NSString *)ids { + if (self = [super init]) { + displayName = [dn copy]; + idString = [ids copy]; + } + return self; +} +- (NSString *)displayName { + return displayName; +} +- (NSString *)idString { + return idString; +} +@end diff --git a/s3/S3Region.h b/s3/S3Region.h new file mode 100644 index 0000000..c5bc6e7 --- /dev/null +++ b/s3/S3Region.h @@ -0,0 +1,39 @@ +// +// S3Region.h +// Arq +// +// Created by Stefan Reitshamer on 2/11/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import + + +@interface S3Region : NSObject { + NSString *bucketNameSuffix; + NSString *legacyBucketNameSuffix; + NSString *locationConstraint; + NSString *endpoint; + NSString *displayName; + double dollarsPerGBMonthStandard; + double dollarsPerGBMonthRRS; +} ++ (NSArray *)allS3Regions; ++ (S3Region *)s3RegionForBucketName:(NSString *)theBucketName; ++ (S3Region *)usStandard; ++ (S3Region *)usWestNorthernCalifornia; ++ (S3Region *)usWestOregon; ++ (S3Region *)euIreland; ++ (S3Region *)asiaPacificSingapore; ++ (S3Region *)asiaPacificTokyo; ++ (S3Region *)southAmericaSaoPaulo; + +- (NSString *)bucketNameSuffix; +- (NSString *)legacyBucketNameSuffix; +- (NSString *)locationConstraint; +- (NSString *)endpoint; +- (NSString *)displayName; +- (double)dollarsPerGBMonthStandard; +- (double)dollarsPerGBMonthRRS; +- (NSString *)bucketNameForAccessKeyID:(NSString *)theAccessKeyID; +@end diff --git a/s3/S3Region.m b/s3/S3Region.m new file mode 100644 index 0000000..90d2c04 --- /dev/null +++ b/s3/S3Region.m @@ -0,0 +1,162 @@ +// +// S3Region.m +// Arq +// +// Created by Stefan Reitshamer on 2/11/12. +// Copyright 2012 __MyCompanyName__. All rights reserved. +// + +#import "S3Region.h" + + +@interface S3Region (internal) +- (id)initWithBucketNameSuffix:(NSString *)theBucketNameSuffix + legacyBucketNameSuffix:(NSString *)theLegacyBucketNameSuffix + locationConstraint:(NSString *)theLocationConstraint + endpoint:(NSString *)theEndpoint + displayName:(NSString *)theDisplayName + dollarsPerGBMonthStandard:(double)theDollarsPerGBMonthStandard + dollarsPerGBMonthRRS:(double)theDollarsPerGBMonthRRS; +@end + +@implementation S3Region ++ (NSArray *)allS3Regions { + NSMutableArray *ret = [NSMutableArray array]; + [ret addObject:[S3Region usStandard]]; + [ret addObject:[S3Region usWestNorthernCalifornia]]; + [ret addObject:[S3Region usWestOregon]]; + [ret addObject:[S3Region euIreland]]; + [ret addObject:[S3Region asiaPacificSingapore]]; + [ret addObject:[S3Region asiaPacificTokyo]]; + [ret addObject:[S3Region southAmericaSaoPaulo]]; + return ret; +} ++ (S3Region *)s3RegionForBucketName:(NSString *)theBucketName { + for (S3Region *region in [S3Region allS3Regions]) { + if ([[region bucketNameSuffix] length] > 0 && ([theBucketName hasSuffix:[region bucketNameSuffix]] || [theBucketName hasSuffix:[region legacyBucketNameSuffix]])) { + return region; + } + } + return [S3Region usStandard]; +} ++ (S3Region *)usStandard { + return [[[S3Region alloc] initWithBucketNameSuffix:@"" + legacyBucketNameSuffix:@"" + locationConstraint:nil + endpoint:@"s3.amazonaws.com" + displayName:@"US Standard" + dollarsPerGBMonthStandard:.125 + dollarsPerGBMonthRRS:.093] autorelease]; +} ++ (S3Region *)usWestNorthernCalifornia { + return [[[S3Region alloc] initWithBucketNameSuffix:@"-us-west-1" + legacyBucketNameSuffix:@"us-west-1" + locationConstraint:@"us-west-1" + endpoint:@"s3-us-west-1.amazonaws.com" + displayName:@"US West (Northern California)" + dollarsPerGBMonthStandard:.140 + dollarsPerGBMonthRRS:.103] autorelease]; +} ++ (S3Region *)usWestOregon { + return [[[S3Region alloc] initWithBucketNameSuffix:@"-us-west-2" + legacyBucketNameSuffix:@"us-west-2" + locationConstraint:@"us-west-2" + endpoint:@"s3-us-west-2.amazonaws.com" + displayName:@"US West (Oregon)" + dollarsPerGBMonthStandard:.125 + dollarsPerGBMonthRRS:.093] autorelease]; +} ++ (S3Region *)euIreland { + return [[[S3Region alloc] initWithBucketNameSuffix:@"-eu" + legacyBucketNameSuffix:@"eu" + locationConstraint:@"EU" + endpoint:@"s3-eu-west-1.amazonaws.com" + displayName:@"EU (Ireland)" + dollarsPerGBMonthStandard:.125 + dollarsPerGBMonthRRS:.093] autorelease]; +} ++ (S3Region *)asiaPacificSingapore { + return [[[S3Region alloc] initWithBucketNameSuffix:@"-ap-southeast-1" + legacyBucketNameSuffix:@"ap-southeast-1" + locationConstraint:@"ap-southeast-1" + endpoint:@"s3-ap-southeast-1.amazonaws.com" + displayName:@"Asia Pacific (Singapore)" + dollarsPerGBMonthStandard:.125 + dollarsPerGBMonthRRS:.093] autorelease]; +} ++ (S3Region *)asiaPacificTokyo { + return [[[S3Region alloc] initWithBucketNameSuffix:@"-ap-northeast-1" + legacyBucketNameSuffix:@"ap-northeast-1" + locationConstraint:@"ap-northeast-1" + endpoint:@"s3-ap-northeast-1.amazonaws.com" + displayName:@"Asia Pacific (Tokyo)" + dollarsPerGBMonthStandard:.130 + dollarsPerGBMonthRRS:.100] autorelease]; +} ++ (S3Region *)southAmericaSaoPaulo { + return [[[S3Region alloc] initWithBucketNameSuffix:@"-sa-east-1" + legacyBucketNameSuffix:@"sa-east-1" + locationConstraint:@"sa-east-1" + endpoint:@"s3-sa-east-1.amazonaws.com" + displayName:@"South America (Sao Paulo)" + dollarsPerGBMonthStandard:.170 + dollarsPerGBMonthRRS:.127] autorelease]; +} + + +- (void)dealloc { + [bucketNameSuffix release]; + [displayName release]; + [super dealloc]; +} + +- (NSString *)bucketNameSuffix { + return bucketNameSuffix; +} +- (NSString *)legacyBucketNameSuffix { + return legacyBucketNameSuffix; +} +- (NSString *)locationConstraint { + return locationConstraint; +} +- (NSString *)endpoint { + return endpoint; +} +- (NSString *)displayName { + return displayName; +} +- (double)dollarsPerGBMonthStandard { + return dollarsPerGBMonthStandard; +} +- (double)dollarsPerGBMonthRRS { + return dollarsPerGBMonthRRS; +} +- (NSString *)bucketNameForAccessKeyID:(NSString *)theAccessKeyID { + return [[theAccessKeyID lowercaseString] stringByAppendingFormat:@"comhaystacksoftwarearq%@", bucketNameSuffix]; +} + +- (NSString *)description { + return displayName; +} +@end + +@implementation S3Region (internal) +- (id)initWithBucketNameSuffix:(NSString *)theBucketNameSuffix + legacyBucketNameSuffix:(NSString *)theLegacyBucketNameSuffix + locationConstraint:(NSString *)theLocationConstraint + endpoint:(NSString *)theEndpoint + displayName:(NSString *)theDisplayName + dollarsPerGBMonthStandard:(double)theDollarsPerGBMonthStandard + dollarsPerGBMonthRRS:(double)theDollarsPerGBMonthRRS { + if (self = [super init]) { + bucketNameSuffix = [theBucketNameSuffix retain]; + legacyBucketNameSuffix = [theLegacyBucketNameSuffix retain]; + locationConstraint = [theLocationConstraint retain]; + endpoint = [theEndpoint retain]; + displayName = [theDisplayName retain]; + dollarsPerGBMonthStandard = theDollarsPerGBMonthStandard; + dollarsPerGBMonthRRS = theDollarsPerGBMonthRRS; + } + return self; +} +@end diff --git a/s3/S3Request.h b/s3/S3Request.h index 49ebb4a..4166f07 100644 --- a/s3/S3Request.h +++ b/s3/S3Request.h @@ -34,26 +34,27 @@ @class S3AuthorizationProvider; @class ServerBlob; @class Blob; +@protocol HTTPConnectionDelegate; +@class HTTPTimeoutSetting; + @interface S3Request : NSObject { NSString *method; - NSString *path; - NSString *s3BucketName; - NSString *queryString; + NSURL *url; S3AuthorizationProvider *sap; BOOL withSSL; BOOL retryOnTransientError; - id urlConnectionDelegate; + HTTPTimeoutSetting *httpTimeoutSetting; + id httpConnectionDelegate; Blob *blob; NSData *blobData; uint64_t length; - NSString *virtualHost; - NSString *virtualPath; NSMutableDictionary *extraHeaders; unsigned long long bytesUploaded; } -- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry; -- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry urlConnectionDelegate:(id)theURLConnectionDelegate; +- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry error:(NSError **)error; +- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry httpConnectionDelegate:(id )theHTTPConnectionDelegate error:(NSError **)error; +- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry httpConnectionDelegate:(id )theHTTPConnectionDelegate httpTimeoutSetting:(HTTPTimeoutSetting *)theTimeoutSetting error:(NSError **)error; - (void)setBlob:(Blob *)theBlob length:(uint64_t)theLength; - (void)setHeader:(NSString *)value forKey:(NSString *)key; - (ServerBlob *)newServerBlob:(NSError **)error; diff --git a/s3/S3Request.m b/s3/S3Request.m index cafd995..0079637 100644 --- a/s3/S3Request.m +++ b/s3/S3Request.m @@ -41,6 +41,9 @@ #import "NSError_extra.h" #import "S3AuthorizationProvider.h" #import "NSError_S3.h" +#import "S3Region.h" +#import "HTTPConnectionFactory.h" +#import "HTTPTimeoutSetting.h" #define INITIAL_RETRY_SLEEP (0.5) @@ -52,35 +55,51 @@ @end @implementation S3Request -- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry { - return [self initWithMethod:theMethod path:thePath queryString:theQueryString authorizationProvider:theSAP useSSL:ssl retryOnTransientError:retry urlConnectionDelegate:nil]; +- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry error:(NSError **)error { + return [self initWithMethod:theMethod path:thePath queryString:theQueryString authorizationProvider:theSAP useSSL:ssl retryOnTransientError:retry httpConnectionDelegate:nil error:error]; } -- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry urlConnectionDelegate:(id)theURLConnectionDelegate { +- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry httpConnectionDelegate:(id )theHTTPConnectionDelegate error:(NSError **)error { + HTTPTimeoutSetting *theTimeoutSetting = [[[HTTPTimeoutSetting alloc] init] autorelease]; + return [self initWithMethod:theMethod path:thePath queryString:theQueryString authorizationProvider:theSAP useSSL:ssl retryOnTransientError:retry httpConnectionDelegate:theHTTPConnectionDelegate httpTimeoutSetting:theTimeoutSetting error:error]; +} +- (id)initWithMethod:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString authorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)ssl retryOnTransientError:(BOOL)retry httpConnectionDelegate:(id )theHTTPConnectionDelegate httpTimeoutSetting:(HTTPTimeoutSetting *)theTimeoutSetting error:(NSError **)error { if (self = [super init]) { method = [theMethod copy]; - path = [thePath copy]; - NSRange s3BucketNameRange = [path rangeOfRegex:@"^/([^/]+)" capture:1]; - if (s3BucketNameRange.location != NSNotFound) { - s3BucketName = [path substringWithRange:s3BucketNameRange]; - } - queryString = [theQueryString copy]; sap = [theSAP retain]; - withSSL = ssl; retryOnTransientError = retry; - urlConnectionDelegate = theURLConnectionDelegate; // Don't retain it. + httpTimeoutSetting = [theTimeoutSetting retain]; + httpConnectionDelegate = theHTTPConnectionDelegate; // Don't retain it. extraHeaders = [[NSMutableDictionary alloc] init]; + + NSString *endpoint = nil; + if ([thePath isEqualToString:@"/"]) { + endpoint = [[S3Region usStandard] endpoint]; + } else { + NSRange s3BucketRange = [thePath rangeOfRegex:@"^/([^/]+)" capture:1]; + NSAssert(s3BucketRange.location != NSNotFound, @"invalid path -- missing s3 bucket name!"); + NSString *s3BucketName = [thePath substringWithRange:s3BucketRange]; + endpoint = [[S3Region s3RegionForBucketName:s3BucketName] endpoint]; + } + if (theQueryString != nil) { + thePath = [thePath stringByAppendingString:theQueryString]; + } + NSString *urlString = [NSString stringWithFormat:@"http%@://%@%@", (ssl ? @"s" : @""), endpoint, thePath]; + url = [[NSURL alloc] initWithString:urlString]; + if (url == nil) { + SETNSERROR([S3Service errorDomain], -1, @"invalid URL: %@", urlString); + [self release]; + return nil; + } } return self; } - (void)dealloc { [method release]; - [path release]; - [queryString release]; + [url release]; + [httpTimeoutSetting release]; [sap release]; [blob release]; [blobData release]; - [virtualHost release]; - [virtualPath release]; [extraHeaders release]; [super dealloc]; } @@ -95,30 +114,6 @@ [extraHeaders setObject:value forKey:key]; } - (ServerBlob *)newServerBlob:(NSError **)error { - [virtualHost release]; - virtualHost = nil; - [virtualPath release]; - virtualPath = nil; - if ([path isEqualToString:@"/"]) { - // List-bucket request. - virtualHost = [@"s3.amazonaws.com" retain]; - virtualPath = [path retain]; - } else { - NSString *pattern = @"^/([^/]+)(.+)$"; - NSRange s3BucketRange = [path rangeOfRegex:pattern capture:1]; - if (s3BucketRange.location == NSNotFound) { - SETNSERROR([S3Service errorDomain], S3SERVICE_INVALID_PARAMETERS, @"invalid path-style path -- missing s3 bucket name"); - return nil; - } - NSRange pathRange = [path rangeOfRegex:pattern capture:2]; - if (pathRange.location == NSNotFound) { - SETNSERROR([S3Service errorDomain], S3SERVICE_INVALID_PARAMETERS, @"invalid path-style path -- missing path"); - return nil; - } - virtualHost = [[[path substringWithRange:s3BucketRange] stringByAppendingString:@".s3.amazonaws.com"] retain]; - virtualPath = [[path substringWithRange:pathRange] retain]; - } - [blobData release]; blobData = nil; if (blob != nil) { @@ -131,7 +126,6 @@ NSTimeInterval sleepTime = INITIAL_RETRY_SLEEP; ServerBlob *sb = nil; NSError *myError = nil; - BOOL loggedRetry = NO; for (;;) { [pool drain]; pool = [[NSAutoreleasePool alloc] init]; @@ -141,18 +135,28 @@ sb = [self newServerBlobOnce:&myError]; if (sb != nil) { break; - } else if ([myError isErrorWithDomain:[S3Service errorDomain] code:ERROR_NOT_FOUND]) { + } + if ([myError isSSLError]) { + HSLogError(@"SSL error: %@", myError); + [myError logSSLCerts]; + } + if ([myError isErrorWithDomain:[S3Service errorDomain] code:ERROR_NOT_FOUND]) { break; + } else if ([myError isErrorWithDomain:[S3Service errorDomain] code:ERROR_TEMPORARY_REDIRECT]) { + NSString *location = [[myError userInfo] objectForKey:@"location"]; + HSLogDebug(@"redirecting %@ to %@", url, location); + [url release]; + url = [[NSURL alloc] initWithString:location]; + if (url == nil) { + HSLogError(@"invalid redirect URL %@", location); + myError = [NSError errorWithDomain:[S3Service errorDomain] code:-1 description:[NSString stringWithFormat:@"invalid redirect URL %@", location]]; + break; + } } else if ([myError isErrorWithDomain:[S3Service errorDomain] code:S3SERVICE_ERROR_AMAZON_ERROR]) { int httpStatusCode = [[[myError userInfo] objectForKey:@"HTTPStatusCode"] intValue]; NSString *amazonCode = [[myError userInfo] objectForKey:@"AmazonCode"]; - if (httpStatusCode == HTTP_MOVED_TEMPORARILY) { - [virtualHost release]; - virtualHost = [[[myError userInfo] objectForKey:@"AmazonEndpoint"] copy]; - HSLogInfo(@"S3 redirect to %@", virtualHost); - - } else if (retryOnTransientError && [amazonCode isEqualToString:@"RequestTimeout"]) { + if (retryOnTransientError && [amazonCode isEqualToString:@"RequestTimeout"]) { transientError = YES; } else if (httpStatusCode == HTTP_INTERNAL_SERVER_ERROR) { @@ -164,27 +168,21 @@ needSleep = YES; } else { - HSLogError(@"%@ %@ (blob %@): %@", method, virtualPath, blob, myError); + HSLogError(@"%@ %@ (blob %@): %@", method, url, blob, myError); break; } - + } else if ([myError isConnectionResetError]) { + transientError = YES; } else if (retryOnTransientError && [myError isTransientError]) { transientError = YES; needSleep = YES; - } else { - HSLogError(@"%@ %@ (blob %@): %@", method, virtualPath, blob, myError); + HSLogError(@"%@ %@ (blob %@): %@", method, url, blob, myError); break; } if (transientError) { - if (!loggedRetry) { - HSLogWarn(@"retrying %@ %@ (request body %@): %@", method, virtualPath, blob, myError); - loggedRetry = YES; - - } else { - HSLogDebug(@"retrying %@ %@ (request body %@): %@", method, virtualPath, blob, myError); - } + HSLogWarn(@"retrying %@ %@ (request body %@): %@", method, url, blob, myError); } if (needSleep) { [NSThread sleepForTimeInterval:sleepTime]; @@ -204,11 +202,7 @@ @implementation S3Request (internal) - (ServerBlob *)newServerBlobOnce:(NSError **)error { - NSString *urlString = [NSString stringWithFormat:@"http%@://%@%@", (withSSL ? @"s" : @""), virtualHost, [virtualPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - if (queryString) { - urlString = [urlString stringByAppendingString:queryString]; - } - id conn = [[[URLConnection alloc] initWithURL:[NSURL URLWithString:urlString] method:method delegate:urlConnectionDelegate] autorelease]; + id conn = [[[HTTPConnectionFactory theFactory] newHTTPConnectionToURL:url method:method httpTimeoutSetting:httpTimeoutSetting httpConnectionDelegate:httpConnectionDelegate] autorelease]; if (conn == nil) { return nil; } @@ -226,39 +220,59 @@ for (NSString *headerKey in [extraHeaders allKeys]) { [conn setRequestHeader:[extraHeaders objectForKey:headerKey] forKey:headerKey]; } - if (![sap setAuthorizationRequestHeaderOnHTTPConnection:conn usingS3BucketName:s3BucketName error:error]) { + if (![sap setAuthorizationRequestHeaderOnHTTPConnection:conn error:error]) { return nil; } bytesUploaded = 0; + + HSLogDebug(@"%@ %@", method, url); + BOOL execRet = [conn executeRequestWithBody:blobData error:error]; if (!execRet) { + HSLogDebug(@"executeRequestWithBody failed"); return nil; } ServerBlob *ret = nil; id bodyStream = [conn newResponseBodyStream:error]; if (bodyStream == nil) { + HSLogDebug(@"newResponseBodyStream failed"); return nil; } - int code = [conn responseCode]; - - if (code >= 200 && code <= 299) { - ret = [[ServerBlob alloc] initWithInputStream:bodyStream mimeType:[conn responseContentType] downloadName:[conn responseDownloadName]]; - [bodyStream release]; - return ret; - } - NSData *response = [bodyStream slurp:error]; + [bodyStream release]; if (response == nil) { return nil; } + int code = [conn responseCode]; + if (code >= 200 && code <= 299) { + ret = [[ServerBlob alloc] initWithData:response mimeType:[conn responseContentType] downloadName:[conn responseDownloadName]]; + HSLogDebug(@"HTTP %d; returning response length=%d", code, [response length]); + return ret; + } + HSLogTrace(@"http response body: %@", [[[NSString alloc] initWithBytes:[response bytes] length:[response length] encoding:NSUTF8StringEncoding] autorelease]); if (code == HTTP_NOT_FOUND) { - SETNSERROR([S3Service errorDomain], ERROR_NOT_FOUND, @"%@ not found", path); + SETNSERROR([S3Service errorDomain], ERROR_NOT_FOUND, @"%@ not found", url); + HSLogDebug(@"returning not-found error"); + return nil; + } + if (code == HTTP_METHOD_NOT_ALLOWED) { + HSLogError(@"%@ 405 error", url); + SETNSERROR([S3Service errorDomain], ERROR_RRS_NOT_FOUND, @"%@ 405 error", url); + } + if (code == HTTP_MOVED_TEMPORARILY) { + NSString *location = [conn responseHeaderForKey:@"Location"]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:location forKey:@"location"]; + NSError *myError = [NSError errorWithDomain:[S3Service errorDomain] code:ERROR_TEMPORARY_REDIRECT userInfo:userInfo]; + if (error != NULL) { + *error = myError; + } + HSLogDebug(@"returning moved-temporarily error"); return nil; } NSError *myError = [NSError amazonErrorWithHTTPStatusCode:code responseBody:response]; - HSLogDebug(@"%@ %@: %@", method, conn, myError); + HSLogDebug(@"%@ %@ error: %@", method, conn, myError); if (error != NULL) { *error = myError; } diff --git a/s3/S3Service.h b/s3/S3Service.h index 8105f87..daf459f 100644 --- a/s3/S3Service.h +++ b/s3/S3Service.h @@ -42,31 +42,20 @@ #define S3_RETRY_SLEEP_GROWTH_FACTOR (1.5) #define S3_MAX_RETRY (5) -enum { - BUCKET_REGION_US_STANDARD = 0, - BUCKET_REGION_US_WEST = 1, - BUCKET_REGION_EU = 2, - BUCKET_REGION_AP_SOUTHEAST_1 = 3, - BUCKET_REGION_AP_NORTHEAST_1 = 4 -}; - enum { S3SERVICE_ERROR_UNEXPECTED_RESPONSE = -51001, S3SERVICE_ERROR_AMAZON_ERROR = -51002, S3SERVICE_INVALID_PARAMETERS = -51003 }; -@interface S3Service : NSObject { +@interface S3Service : NSObject { S3AuthorizationProvider *sap; BOOL useSSL; BOOL retryOnTransientError; } + (NSString *)errorDomain; -+ (NSString *)displayNameForBucketRegion:(int)region; -+ (NSString *)s3BucketNameForAccessKeyID:(NSString *)theAccessKeyId region:(int)s3BucketRegion; -+ (NSArray *)s3BucketNamesForAccessKeyID:(NSString *)theAccessKeyId; -+ (int)s3BucketRegionForS3BucketName:(NSString *)s3BucketName; - (id)initWithS3AuthorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)useSSL retryOnTransientError:(BOOL)retry; +- (S3Owner *)s3Owner:(NSError **)error; - (NSArray *)s3BucketNames:(NSError **)error; - (BOOL)s3BucketExists:(NSString *)s3BucketName; @@ -82,4 +71,6 @@ enum { - (BOOL)aclXMLData:(NSData **)aclXMLData atPath:(NSString *)path error:(NSError **)error; - (BOOL)acl:(int *)acl atPath:(NSString *)path error:(NSError **)error; + +- (S3AuthorizationProvider *)s3AuthorizationProvider; @end diff --git a/s3/S3Service.m b/s3/S3Service.m index 0574110..74d2114 100644 --- a/s3/S3Service.m +++ b/s3/S3Service.m @@ -35,6 +35,7 @@ #import "Blob.h" #import "RegexKitLite.h" #import "NSError_S3.h" +#import "S3Owner.h" #import "S3Lister.h" #import "S3AuthorizationProvider.h" #import "S3Service.h" @@ -65,58 +66,7 @@ + (NSString *)errorDomain { return @"S3ServiceErrorDomain"; } -+ (NSString *)displayNameForBucketRegion:(int)region { - switch (region) { - case BUCKET_REGION_US_STANDARD: - return @"US Standard"; - case BUCKET_REGION_US_WEST: - return @"US-West (Northern California)"; - case BUCKET_REGION_AP_SOUTHEAST_1: - return @"Asia Pacific (Singapore)"; - case BUCKET_REGION_AP_NORTHEAST_1: - return @"Asia Pacific (Japan)"; - case BUCKET_REGION_EU: - return @"EU (Ireland)"; - } - NSAssert(NO, @"invalid S3 bucket region"); - return nil; -} -+ (NSString *)s3BucketNameForAccessKeyID:(NSString *)theAccessKeyID region:(int)s3BucketRegion { - NSString *regionSuffix = @""; - if (s3BucketRegion == BUCKET_REGION_US_WEST) { - regionSuffix = @".us-west-1"; - } else if (s3BucketRegion == BUCKET_REGION_AP_SOUTHEAST_1) { - regionSuffix = @".ap-southeast-1"; - } else if (s3BucketRegion == BUCKET_REGION_AP_NORTHEAST_1) { - regionSuffix = @".ap-northeast-1"; - } else if (s3BucketRegion == BUCKET_REGION_AP_NORTHEAST_1) { - regionSuffix = @".ap-northeast-1"; - } else if (s3BucketRegion == BUCKET_REGION_EU) { - regionSuffix = @".eu"; - } - return [NSString stringWithFormat:@"%@.com.haystacksoftware.arq%@", [theAccessKeyID lowercaseString], regionSuffix]; -} -+ (int)s3BucketRegionForS3BucketName:(NSString *)s3BucketName { - if ([s3BucketName hasSuffix:@"com.haystacksoftware.arq.eu"]) { - return BUCKET_REGION_EU; - } else if ([s3BucketName hasSuffix:@"com.haystacksoftware.arq.ap-southeast-1"]) { - return BUCKET_REGION_AP_SOUTHEAST_1; - } else if ([s3BucketName hasSuffix:@"com.haystacksoftware.arq.ap-northeast-1"]) { - return BUCKET_REGION_AP_NORTHEAST_1; - } else if ([s3BucketName hasSuffix:@"com.haystacksoftware.arq.us-west-1"]) { - return BUCKET_REGION_US_WEST; - } - return BUCKET_REGION_US_STANDARD; -} -+ (NSArray *)s3BucketNamesForAccessKeyID:(NSString *)theAccessKeyId { - return [NSArray arrayWithObjects: - [S3Service s3BucketNameForAccessKeyID:theAccessKeyId region:BUCKET_REGION_US_STANDARD], - [S3Service s3BucketNameForAccessKeyID:theAccessKeyId region:BUCKET_REGION_US_WEST], - [S3Service s3BucketNameForAccessKeyID:theAccessKeyId region:BUCKET_REGION_EU], - [S3Service s3BucketNameForAccessKeyID:theAccessKeyId region:BUCKET_REGION_AP_SOUTHEAST_1], - [S3Service s3BucketNameForAccessKeyID:theAccessKeyId region:BUCKET_REGION_AP_NORTHEAST_1], - nil]; -} + - (id)initWithS3AuthorizationProvider:(S3AuthorizationProvider *)theSAP useSSL:(BOOL)isUseSSL retryOnTransientError:(BOOL)retry { if (self = [super init]) { sap = [theSAP retain]; @@ -129,6 +79,36 @@ [sap release]; [super dealloc]; } +- (S3Owner *)s3Owner:(NSError **)error { + if (error) { + *error = 0; + } + NSXMLDocument *doc = [self listBuckets:error]; + if (!doc) { + return nil; + } + NSXMLElement *rootElem = [doc rootElement]; + NSArray *idNodes = [rootElem nodesForXPath:@"//ListAllMyBucketsResult/Owner/ID" error:error]; + if (!idNodes) { + return nil; + } + if ([idNodes count] == 0) { + HSLogError(@"ListAllMyBucketsResult/Owner/ID node not found"); + return nil; + } + NSXMLNode *ownerIDNode = [idNodes objectAtIndex:0]; + NSArray *displayNameNodes = [rootElem nodesForXPath:@"//ListAllMyBucketsResult/Owner/DisplayName" error:error]; + if (!displayNameNodes) { + return nil; + } + if ([displayNameNodes count] == 0) { + HSLogError(@"ListAllMyBucketsResult/Owner/DisplayName not found"); + return nil; + } + NSXMLNode *displayNameNode = [displayNameNodes objectAtIndex:0]; + HSLogDebug(@"s3 owner ID: %@", [displayNameNode stringValue]); + return [[[S3Owner alloc] initWithDisplayName:[displayNameNode stringValue] idString:[ownerIDNode stringValue]] autorelease]; +} - (NSArray *)s3BucketNames:(NSError **)error { NSXMLDocument *doc = [self listBuckets:error]; if (!doc) { @@ -192,7 +172,10 @@ } - (BOOL)containsBlob:(BOOL *)contains atPath:(NSString *)path dataSize:(unsigned long long *)dataSize error:(NSError **)error { BOOL ret = YES; - S3Request *s3r = [[S3Request alloc] initWithMethod:@"HEAD" path:path queryString:nil authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError]; + S3Request *s3r = [[S3Request alloc] initWithMethod:@"HEAD" path:path queryString:nil authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError error:error]; + if (s3r == nil) { + return NO; + } NSError *myError = nil; ServerBlob *sb = [s3r newServerBlob:&myError]; if (sb != nil) { @@ -201,6 +184,9 @@ } else if ([myError isErrorWithDomain:[S3Service errorDomain] code:ERROR_NOT_FOUND]) { *contains = NO; HSLogDebug(@"S3 path %@ does NOT exist", path); + } else if ([myError isErrorWithDomain:[S3Service errorDomain] code:ERROR_RRS_NOT_FOUND]) { + *contains = NO; + HSLogDebug(@"S3 path %@ returns 405 error", path); } else { *contains = NO; ret = NO; @@ -222,7 +208,10 @@ } - (ServerBlob *)newServerBlobAtPath:(NSString *)path error:(NSError **)error { HSLogDebug(@"getting %@", path); - S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:path queryString:nil authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError]; + S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:path queryString:nil authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError error:error]; + if (s3r == nil) { + return nil; + } ServerBlob *sb = [s3r newServerBlob:error]; [s3r release]; return sb; @@ -230,7 +219,10 @@ - (BOOL)aclXMLData:(NSData **)aclXMLData atPath:(NSString *)path error:(NSError **)error { *aclXMLData = nil; HSLogDebug(@"getting %@", path); - S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:path queryString:@"?acl" authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError]; + S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:path queryString:@"?acl" authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError error:error]; + if (s3r == nil) { + return NO; + } ServerBlob *sb = [s3r newServerBlob:error]; [s3r release]; if (sb == nil) { @@ -260,11 +252,24 @@ } return ret; } + +- (S3AuthorizationProvider *)s3AuthorizationProvider { + return sap; +} + + +#pragma mark NSCopying +- (id)copyWithZone:(NSZone *)zone { + return [[S3Service alloc] initWithS3AuthorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError]; +} @end @implementation S3Service (internal) - (NSXMLDocument *)listBuckets:(NSError **)error { - S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:@"/" queryString:nil authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError]; + S3Request *s3r = [[S3Request alloc] initWithMethod:@"GET" path:@"/" queryString:nil authorizationProvider:sap useSSL:useSSL retryOnTransientError:retryOnTransientError error:error]; + if (s3r == nil) { + return nil; + } ServerBlob *sb = [s3r newServerBlob:error]; [s3r release]; if (sb == nil) { @@ -275,17 +280,26 @@ if (data == nil) { return nil; } - return [[[NSXMLDocument alloc] initWithData:data options:0 error:error] autorelease]; + NSError *myError = nil; + NSXMLDocument *ret = [[[NSXMLDocument alloc] initWithData:data options:0 error:&myError] autorelease]; + if (ret == nil) { + HSLogDebug(@"error parsing List Buckets result XML %@", [[[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding] autorelease]); + SETNSERROR([S3Service errorDomain], [myError code], @"error parsing S3 List Buckets result XML: %@", [myError description]); + } + return ret; } - (BOOL)internalACL:(int *)acl atPath:(NSString *)path error:(NSError **)error { NSData *aclData; if (![self aclXMLData:&aclData atPath:path error:error]) { return NO; } - NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData:aclData options:0 error:error] autorelease]; + NSError *myError = nil; + NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData:aclData options:0 error:&myError] autorelease]; if (!xmlDoc) { + SETNSERROR([S3Service errorDomain], [myError code], @"error parsing S3 Get ACL response: %@", myError); return NO; } + HSLogTrace(@"ACL XML: %@", xmlDoc); NSArray *grants = [xmlDoc nodesForXPath:@"AccessControlPolicy/AccessControlList/Grant" error:error]; if (!grants) { return NO; diff --git a/shared/DNS_SDErrors.h b/shared/DNS_SDErrors.h new file mode 100644 index 0000000..ccee90d --- /dev/null +++ b/shared/DNS_SDErrors.h @@ -0,0 +1,40 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + + +@interface DNS_SDErrors : NSObject { + +} ++ (NSString *)descriptionForDNS_SDError:(int)code; +@end diff --git a/shared/DNS_SDErrors.m b/shared/DNS_SDErrors.m new file mode 100644 index 0000000..c614aa6 --- /dev/null +++ b/shared/DNS_SDErrors.m @@ -0,0 +1,101 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "dns_sd.h" +#import "DNS_SDErrors.h" + + +@implementation DNS_SDErrors ++ (NSString *)descriptionForDNS_SDError:(int)code { + switch(code) { + case kDNSServiceErr_NoError: + return @"DNS service no error"; + case kDNSServiceErr_Unknown: + return @"DNS service unknown error"; + case kDNSServiceErr_NoSuchName: + return @"DNS service: no such name"; + case kDNSServiceErr_NoMemory: + return @"DNS service: no memory"; + case kDNSServiceErr_BadParam: + return @"DNS service: bad parameter"; + case kDNSServiceErr_BadReference: + return @"DNS service: bad reference"; + case kDNSServiceErr_BadState: + return @"DNS service: bad state"; + case kDNSServiceErr_BadFlags: + return @"DNS service: bad flags"; + case kDNSServiceErr_Unsupported: + return @"DNS service: unsupported"; + case kDNSServiceErr_NotInitialized: + return @"DNS service: not initialized"; + case kDNSServiceErr_AlreadyRegistered: + return @"DNS service: already registered"; + case kDNSServiceErr_NameConflict: + return @"DNS service: name conflict"; + case kDNSServiceErr_Invalid: + return @"DNS service: invalid"; + case kDNSServiceErr_Firewall: + return @"DNS service: firewall error"; + case kDNSServiceErr_Incompatible: + return @"DNS service: incompatible"; + case kDNSServiceErr_BadInterfaceIndex: + return @"DNS service: bad interface index"; + case kDNSServiceErr_Refused: + return @"DNS service: refused"; + case kDNSServiceErr_NoSuchRecord: + return @"DNS service: no such record"; + case kDNSServiceErr_NoAuth: + return @"DNS service: no auth"; + case kDNSServiceErr_NoSuchKey: + return @"DNS service: no such key"; + case kDNSServiceErr_NATTraversal: + return @"DNS service: NAT traversal error"; + case kDNSServiceErr_DoubleNAT: + return @"DNS service: double NAT error"; + case kDNSServiceErr_BadTime: + return @"DNS service: bad time"; + case kDNSServiceErr_BadSig: + return @"DNS service: bad sig"; + case kDNSServiceErr_BadKey: + return @"DNS service: bad key"; + case kDNSServiceErr_Transient: + return @"DNS service: transient"; + case kDNSServiceErr_ServiceNotRunning: + return @"DNS service not running"; + case kDNSServiceErr_NATPortMappingUnsupported: + return @"DNS service: NAT port mapping unsupported"; + case kDNSServiceErr_NATPortMappingDisabled: + return @"DNS service: NAT port mapping disabled"; + } + return @"unknown DNS service error"; +} +@end diff --git a/shared/NSErrorCodes.h b/shared/NSErrorCodes.h index 666e7d1..c9515ed 100644 --- a/shared/NSErrorCodes.h +++ b/shared/NSErrorCodes.h @@ -43,4 +43,8 @@ #define ERROR_ACCESS_DENIED (-12) #define ERROR_MISSING_REQUIRED_OBJECT (-13) #define ERROR_PAUSE_REQUESTED (-14) -#define ERROR_HELPER_CRASHED (-15) \ No newline at end of file +#define ERROR_HELPER_CRASHED (-15) +#define ERROR_TEMPORARY_REDIRECT (-16) +#define ERROR_RRS_NOT_FOUND (-17) +#define ERROR_INVALID_FILE (-18) +#define ERROR_DELAYS_IN_S3_EVENTUAL_CONSISTENCY (-19) diff --git a/shared/NSError_extra.h b/shared/NSError_extra.h index 035c5f3..43a3363 100644 --- a/shared/NSError_extra.h +++ b/shared/NSError_extra.h @@ -36,5 +36,8 @@ + (NSError *)errorWithDomain:(NSString *)domain code:(NSInteger)code description:(NSString *)theDescription; - (id)initWithDomain:(NSString *)domain code:(NSInteger)code description:(NSString *)theDescription; - (BOOL)isErrorWithDomain:(NSString *)theDomain code:(int)theCode; +- (BOOL)isConnectionResetError; - (BOOL)isTransientError; +- (BOOL)isSSLError; +- (void)logSSLCerts; @end diff --git a/shared/NSError_extra.m b/shared/NSError_extra.m index e0ead57..75bd18e 100644 --- a/shared/NSError_extra.m +++ b/shared/NSError_extra.m @@ -30,26 +30,55 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#import +#import #import "NSError_extra.h" +#import "NSErrorCodes.h" + @implementation NSError (extra) + (NSError *)errorWithDomain:(NSString *)domain code:(NSInteger)code description:(NSString *)theDescription { + if (theDescription == nil) { + theDescription = @"(missing description)"; + } return [NSError errorWithDomain:domain code:code userInfo:[NSDictionary dictionaryWithObject:theDescription forKey:NSLocalizedDescriptionKey]]; } - (id)initWithDomain:(NSString *)domain code:(NSInteger)code description:(NSString *)theDescription { + if (theDescription == nil) { + theDescription = @"(missing description)"; + } return [self initWithDomain:domain code:code userInfo:[NSDictionary dictionaryWithObject:theDescription forKey:NSLocalizedDescriptionKey]]; } - (BOOL)isErrorWithDomain:(NSString *)theDomain code:(int)theCode { return [self code] == theCode && [[self domain] isEqualToString:theDomain]; } +- (BOOL)isConnectionResetError { + if ([[self domain] isEqualToString:@"UnixErrorDomain"] || [[self domain] isEqualToString:@"NSPOSIXErrorDomain"]) { + if ([self code] == ENETRESET + || [self code] == ECONNRESET) { + return YES; + } + } + return NO; +} - (BOOL)isTransientError { - if ([[self domain] isEqualToString:@"UnixErrorDomain"] && [self code] == ETIMEDOUT) { - return YES; + if ([[self domain] isEqualToString:@"UnixErrorDomain"] || [[self domain] isEqualToString:@"NSPOSIXErrorDomain"]) { + if ([self code] == ENETDOWN + || [self code] == EADDRNOTAVAIL + || [self code] == ENETUNREACH + || [self code] == ENETRESET + || [self code] == ECONNABORTED + || [self code] == ECONNRESET + || [self code] == EISCONN + || [self code] == ENOTCONN + || [self code] == ETIMEDOUT + || [self code] == EHOSTUNREACH + || [self code] == EHOSTDOWN + || [self code] == EPIPE) { + return YES; + } } - if ([[self domain] isEqualToString:@"NSPOSIXErrorDomain"] && [self code] == ETIMEDOUT) { - return YES; - } - if ([[self domain] isEqualToString:@"NSPOSIXErrorDomain"] && [self code] == ENOTCONN) { + if ([[self domain] isEqualToString:(NSString *)kCFErrorDomainCFNetwork]) { return YES; } if ([[self domain] isEqualToString:NSURLErrorDomain]) { @@ -63,7 +92,55 @@ return YES; } } + if ([[self domain] isEqualToString:@"NSOSStatusErrorDomain"] && [self code] <= errSSLProtocol && [self code] >= errSSLLast) { + return YES; + } + if ([self isSSLError]) { + return YES; + } + if ([self code] == ERROR_TIMEOUT) { + return YES; + } HSLogDebug(@"%@ not a transient error", self); return NO; } +- (BOOL)isSSLError { + return [[self domain] isEqualToString:NSURLErrorDomain] + && [self code] <= NSURLErrorSecureConnectionFailed + && [self code] >= NSURLErrorClientCertificateRejected; +} +- (void)logSSLCerts { + NSArray *certs = [[self userInfo] objectForKey:@"NSErrorPeerCertificateChainKey"]; + for (NSUInteger index = 0; index < [certs count]; index++) { + SecCertificateRef secCert = (SecCertificateRef)[certs objectAtIndex:index]; + CFStringRef commonName = NULL; + if (SecCertificateCopyCommonName(secCert, &commonName) == 0 && commonName != NULL) { + HSLog(@"SSL cert common name: %@", (NSString *)commonName); + CFRelease(commonName); + } else { + HSLog(@"error getting SSL cert's common name"); + } + SecKeyRef key = NULL; + if (SecCertificateCopyPublicKey(secCert, &key) == 0 && key != NULL) { + CSSM_CSP_HANDLE cspHandle; + if (SecKeyGetCSPHandle(key, &cspHandle) == 0) { + HSLog(@"SSL cert CSP handle: %d", (long)cspHandle); + } else { + HSLog(@"error getting SSL cert's key's CSP handle"); + } + const CSSM_KEY *cssmKey; + if (SecKeyGetCSSMKey(key, &cssmKey) == 0) { + NSData *keyHeaderData = [NSData dataWithBytes:(const unsigned char *)&(cssmKey->KeyHeader) length:sizeof(CSSM_KEYHEADER)]; + HSLog(@"SSL cert CSSM key header: %@", keyHeaderData); + NSData *keyData = [NSData dataWithBytes:(const unsigned char *)cssmKey->KeyData.Data length:cssmKey->KeyData.Length]; + HSLog(@"SSL cert CSSM key data: %@", keyData); + } else { + HSLog(@"error getting SSL cert's key's CSM key"); + } + CFRelease(key); + } else { + HSLog(@"error getting SSL cert's public key"); + } + } +} @end diff --git a/shared/Sysctl.h b/shared/Sysctl.h new file mode 100644 index 0000000..7d61d9d --- /dev/null +++ b/shared/Sysctl.h @@ -0,0 +1,40 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + + +@interface Sysctl : NSObject { + +} ++ (BOOL)networkBytesIn:(unsigned long long *)bytesIn bytesOut:(unsigned long long *)bytsOut error:(NSError **)error; +@end diff --git a/shared/Sysctl.m b/shared/Sysctl.m new file mode 100644 index 0000000..31eed33 --- /dev/null +++ b/shared/Sysctl.m @@ -0,0 +1,78 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#import "Sysctl.h" +#import "SetNSError.h" + +@implementation Sysctl ++ (BOOL)networkBytesIn:(unsigned long long *)bytesIn bytesOut:(unsigned long long *)bytesOut error:(NSError **)error { + int mib[] = { + CTL_NET, + PF_ROUTE, + 0, + 0, + NET_RT_IFLIST2, + 0 + }; + size_t len; + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + int errnum = errno; + SETNSERROR(@"UnixErrorDomain", errnum, @"sysctl: %s", strerror(errnum)); + return NO; + } + char *buf = (char *)malloc(len); + if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { + int errnum = errno; + SETNSERROR(@"UnixErrorDomain", errnum, @"sysctl: %s", strerror(errnum)); + return NO; + } + char *lim = buf + len; + char *next = NULL; + *bytesIn = 0; + *bytesOut = 0; + for (next = buf; next < lim; ) { + struct if_msghdr *ifm = (struct if_msghdr *)next; + next += ifm->ifm_msglen; + if (ifm->ifm_type == RTM_IFINFO2) { + struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; + *bytesIn += if2m->ifm_data.ifi_ibytes; + *bytesOut += if2m->ifm_data.ifi_obytes; + } + } + free(buf); + return YES; +} +@end